use std::collections::{HashMap, HashSet};
use duckdb::params;
use crate::analyses::code_health::{CodeHealthRow, HealthScanCtx, run_code_health_scoped};
use crate::analyses::delta_health::{Outcome, classify, outcome_for, run_function_metrics};
use crate::analyses::lineage;
use crate::facts::FactsDb;
use crate::repo::Repo;
use crate::{CodeLoreError, Options, Result};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct EffortExposureRow {
pub band: String,
pub files: u32,
pub loc_share_pct: f64,
pub commit_share_pct: f64,
pub churn_share_pct: f64,
pub commit_share_ci_low: f64,
pub commit_share_ci_high: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub churn_share_improving_pct: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub churn_share_degrading_pct: Option<f64>,
}
const BANDS_DDL: &str = "
CREATE OR REPLACE TEMPORARY TABLE eh_bands_v1 (
path TEXT NOT NULL,
band TEXT NOT NULL,
sloc BIGINT NOT NULL DEFAULT 0
);
";
fn fetch_sloc_map(db: &FactsDb) -> Result<HashMap<String, i64>> {
let mut stmt = db
.conn()
.prepare("SELECT path, COALESCE(SUM(sloc), 0) FROM complexity_metrics GROUP BY path")
.map_err(|e| CodeLoreError::Analysis(format!("prepare sloc query: {e}")))?;
let rows = stmt
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?)))
.map_err(|e| CodeLoreError::Analysis(format!("query sloc: {e}")))?;
rows.collect::<std::result::Result<HashMap<_, _>, _>>()
.map_err(|e| CodeLoreError::Analysis(format!("collect sloc: {e}")))
}
fn populate_bands_table(
db: &FactsDb,
health: &[CodeHealthRow],
sloc_map: &HashMap<String, i64>,
) -> Result<()> {
db.conn()
.execute(BANDS_DDL, [])
.map_err(|e| CodeLoreError::Analysis(format!("create eh_bands_v1: {e}")))?;
let mut ins = db
.conn()
.prepare("INSERT INTO eh_bands_v1 (path, band, sloc) VALUES (?, ?, ?)")
.map_err(|e| CodeLoreError::Analysis(format!("prepare eh_bands_v1 insert: {e}")))?;
for row in health {
let sloc = sloc_map.get(&row.path).copied().unwrap_or(0);
ins.execute(params![row.path, row.band, sloc])
.map_err(|e| CodeLoreError::Analysis(format!("insert eh_bands_v1 row: {e}")))?;
}
Ok(())
}
pub fn run_effort_exposure(db: &FactsDb, opts: &Options) -> Result<Vec<EffortExposureRow>> {
let health = run_code_health_scoped(
db,
&opts.with_no_row_limit(),
&HealthScanCtx::head_default(),
)?;
run_effort_exposure_with_health(db, opts, &health)
}
#[allow(clippy::too_many_lines)]
pub fn run_effort_exposure_with_health(
db: &FactsDb,
opts: &Options,
health: &[CodeHealthRow],
) -> Result<Vec<EffortExposureRow>> {
if health.is_empty() {
return Ok(vec![]);
}
let sloc_map = fetch_sloc_map(db)?;
populate_bands_table(db, health, &sloc_map)?;
lineage::materialize_if_needed(db, opts)?;
let src = lineage::source_table(opts);
let wd = opts.window_days;
let now_anchor = crate::analyses::query::clamped_now_anchor("date");
let sql = format!("
WITH win AS (
SELECT rev FROM commits
WHERE date >= (SELECT {now_anchor} FROM commits) - INTERVAL '{wd} days'
),
band_files AS (
SELECT band,
COUNT(*) AS files,
COALESCE(SUM(sloc), 0) AS band_sloc
FROM eh_bands_v1
GROUP BY band
),
band_commits AS (
SELECT b.band,
COUNT(DISTINCT c.rev) AS n_commits
FROM {src} c
INNER JOIN win USING (rev)
INNER JOIN eh_bands_v1 b ON b.path = c.path
GROUP BY b.band
),
band_churn AS (
SELECT b.band,
COALESCE(SUM(c.loc_added + c.loc_deleted), 0) AS churn
FROM {src} c
INNER JOIN win USING (rev)
INNER JOIN eh_bands_v1 b ON b.path = c.path
GROUP BY b.band
),
total_sloc AS (SELECT COALESCE(SUM(sloc), 0) AS v FROM eh_bands_v1),
-- total_commits/total_churn join eh_bands_v1 so their denominators cover
-- the same banded population as the numerators (see this module's docs).
total_commits AS (
SELECT COUNT(DISTINCT c.rev) AS v
FROM {src} c
INNER JOIN win USING (rev)
INNER JOIN eh_bands_v1 b ON b.path = c.path
),
total_churn AS (
SELECT COALESCE(SUM(c.loc_added + c.loc_deleted), 0) AS v
FROM {src} c
INNER JOIN win USING (rev)
INNER JOIN eh_bands_v1 b ON b.path = c.path
)
SELECT
bf.band,
bf.files::INTEGER AS files,
100.0 * bf.band_sloc / NULLIF((SELECT v FROM total_sloc), 0) AS loc_share_pct,
100.0 * COALESCE(bc.n_commits, 0) / NULLIF((SELECT v FROM total_commits), 0) AS commit_share_pct,
100.0 * COALESCE(bch.churn, 0) / NULLIF((SELECT v FROM total_churn), 0) AS churn_share_pct,
COALESCE(bc.n_commits, 0) AS k_commits,
(SELECT v FROM total_commits) AS n_commits
FROM band_files bf
LEFT JOIN band_commits bc ON bc.band = bf.band
LEFT JOIN band_churn bch ON bch.band = bf.band
ORDER BY CASE bf.band WHEN 'red' THEN 1 WHEN 'yellow' THEN 2 ELSE 3 END
");
let mut stmt = db
.conn()
.prepare(&sql)
.map_err(|e| CodeLoreError::Analysis(format!("prepare effort-exposure: {e}")))?;
let raw = stmt
.query_map([], |r| {
Ok((
r.get::<_, String>(0)?, r.get::<_, u32>(1)?, r.get::<_, Option<f64>>(2)?.unwrap_or(0.0), r.get::<_, Option<f64>>(3)?.unwrap_or(0.0), r.get::<_, Option<f64>>(4)?.unwrap_or(0.0), r.get::<_, i64>(5)?, r.get::<_, i64>(6)?, ))
})
.map_err(|e| CodeLoreError::Analysis(format!("query effort-exposure: {e}")))?;
let mut out = Vec::new();
for r in raw {
let (band, files, loc_share_pct, commit_share_pct, churn_share_pct, k, n) =
r.map_err(|e| CodeLoreError::Analysis(format!("collect effort-exposure: {e}")))?;
let (commit_share_ci_low, commit_share_ci_high) =
crate::stats::wilson_ci(u32::try_from(k).unwrap_or(0), u32::try_from(n).unwrap_or(0));
out.push(EffortExposureRow {
band,
files,
loc_share_pct,
commit_share_pct,
churn_share_pct,
commit_share_ci_low,
commit_share_ci_high,
churn_share_improving_pct: None,
churn_share_degrading_pct: None,
});
}
Ok(out)
}
pub fn run_effort_exposure_decomposed<R: Repo>(
db: &FactsDb,
repo: &R,
opts: &Options,
health: &[CodeHealthRow],
) -> Result<Vec<EffortExposureRow>> {
let mut rows = run_effort_exposure_with_health(db, opts, health)?;
let Some(red_idx) = rows.iter().position(|r| r.band == "red") else {
return Ok(rows); };
let red_paths: HashSet<String> = health
.iter()
.filter(|r| r.band == "red")
.map(|r| r.path.clone())
.collect();
let (improving_pct, degrading_pct) = decompose_red_churn(db, repo, opts, &red_paths)?;
rows[red_idx].churn_share_improving_pct = Some(improving_pct);
rows[red_idx].churn_share_degrading_pct = Some(degrading_pct);
Ok(rows)
}
pub fn run_effort_exposure_decomposed_scan<R: Repo>(
db: &FactsDb,
repo: &R,
opts: &Options,
) -> Result<Vec<EffortExposureRow>> {
let health = run_code_health_scoped(
db,
&opts.with_no_row_limit(),
&HealthScanCtx::head_default(),
)?;
run_effort_exposure_decomposed(db, repo, opts, &health)
}
fn window_total_churn(db: &FactsDb, src: &str, wd: u32) -> Result<f64> {
let now_anchor = crate::analyses::query::clamped_now_anchor("date");
let sql = format!(
"SELECT COALESCE(SUM(c.loc_added + c.loc_deleted), 0)::DOUBLE
FROM {src} c
INNER JOIN eh_bands_v1 b ON b.path = c.path
WHERE c.rev IN (
SELECT rev FROM commits
WHERE date >= (SELECT {now_anchor} FROM commits) - INTERVAL '{wd} days'
)"
);
db.conn()
.query_row(&sql, [], |r| r.get::<_, f64>(0))
.map_err(|e| CodeLoreError::Analysis(format!("query window total churn: {e}")))
}
fn red_window_churn(db: &FactsDb, src: &str, wd: u32) -> Result<HashMap<String, f64>> {
let now_anchor = crate::analyses::query::clamped_now_anchor("date");
let sql = format!(
"SELECT c.path, COALESCE(SUM(c.loc_added + c.loc_deleted), 0)::DOUBLE AS churn
FROM {src} c
INNER JOIN eh_bands_v1 b ON b.path = c.path AND b.band = 'red'
WHERE c.rev IN (
SELECT rev FROM commits
WHERE date >= (SELECT {now_anchor} FROM commits) - INTERVAL '{wd} days'
)
GROUP BY c.path"
);
let mut stmt = db
.conn()
.prepare(&sql)
.map_err(|e| CodeLoreError::Analysis(format!("prepare red window churn: {e}")))?;
let rows = stmt
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, f64>(1)?)))
.map_err(|e| CodeLoreError::Analysis(format!("query red window churn: {e}")))?;
rows.collect::<std::result::Result<HashMap<_, _>, _>>()
.map_err(|e| CodeLoreError::Analysis(format!("collect red window churn: {e}")))
}
pub fn window_start_rev(db: &FactsDb, wd: u32) -> Result<Option<String>> {
let now_anchor = crate::analyses::query::clamped_now_anchor("date");
let sql = format!(
"SELECT rev FROM commits
WHERE date < (SELECT {now_anchor} FROM commits) - INTERVAL '{wd} days'
ORDER BY date DESC, rev DESC
LIMIT 1"
);
match db.conn().query_row(&sql, [], |r| r.get::<_, String>(0)) {
Ok(rev) => Ok(Some(rev)),
Err(duckdb::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(CodeLoreError::Analysis(format!(
"query window-start rev: {e}"
))),
}
}
type FnMetrics = HashMap<String, (u32, f64)>;
fn head_function_metrics(
db: &FactsDb,
paths: &HashSet<String, impl std::hash::BuildHasher>,
) -> Result<HashMap<String, FnMetrics>> {
let mut out: HashMap<String, FnMetrics> = HashMap::new();
for row in run_function_metrics(db)? {
if paths.contains(&row.path) {
out.entry(row.path)
.or_default()
.insert(row.name, (row.loc, row.cyclomatic));
}
}
Ok(out)
}
pub fn window_net_movement<R: Repo>(
db: &FactsDb,
repo: &R,
paths: &HashSet<String, impl std::hash::BuildHasher>,
window_start: Option<&str>,
) -> Result<HashMap<String, f64>> {
let head_fns = head_function_metrics(db, paths)?;
let empty: FnMetrics = HashMap::new();
let mut out = HashMap::with_capacity(paths.len());
for path in paths {
let head = head_fns.get(path).unwrap_or(&empty);
let base =
window_start.map_or_else(FnMetrics::new, |rev| base_function_metrics(repo, rev, path));
out.insert(path.clone(), net_movement(head, &base));
}
Ok(out)
}
fn base_function_metrics<R: Repo>(repo: &R, rev: &str, path: &str) -> FnMetrics {
use crate::complexity::{Tier1Language, compute_for_file};
let mut out: FnMetrics = HashMap::new();
let Some(lang) = Tier1Language::from_path(path) else {
return out;
};
let source = match repo.read_blob_at(rev, path) {
Ok(Some(b)) => b,
Ok(None) => return out,
Err(e) => {
tracing::warn!("effort-exposure: blob read failed for {path} at {rev}: {e}");
return out;
}
};
if source.len() > crate::constants::DEFAULT_MAX_AST_FILE_BYTES {
return out;
}
let entities = match compute_for_file(std::path::Path::new(path), source, lang) {
Ok(v) => v,
Err(e) => {
tracing::warn!("effort-exposure: parse error {path} at {rev}: {e}");
return out;
}
};
for e in entities {
if e.kind == "function" || e.kind == "method" {
let entry = out.entry(e.name).or_insert((0, 0.0));
entry.0 = entry.0.max(e.loc);
entry.1 = entry.1.max(e.cyclomatic);
}
}
out
}
pub(crate) fn net_movement(head: &FnMetrics, base: &FnMetrics) -> f64 {
let mut good_w = 0.0_f64;
let mut bad_w = 0.0_f64;
let names: HashSet<&String> = head.keys().chain(base.keys()).collect();
for name in names {
let b = base.get(name);
let h = head.get(name);
if let (Some(b), Some(h)) = (b, h)
&& b == h
{
continue;
}
let before = b.map(|&(loc, cyc)| classify(loc, cyc, false));
let after = h.map(|&(loc, cyc)| classify(loc, cyc, false));
let weight = f64::from(h.or(b).map_or(0, |&(loc, _)| loc));
match outcome_for(before, after) {
Outcome::Good => good_w += weight,
Outcome::Bad => bad_w += weight,
Outcome::Neutral => {}
}
}
good_w - bad_w
}
fn file_improved(head: &FnMetrics, base: &FnMetrics) -> bool {
net_movement(head, base) > 0.0
}
fn decompose_red_churn<R: Repo>(
db: &FactsDb,
repo: &R,
opts: &Options,
red_paths: &HashSet<String>,
) -> Result<(f64, f64)> {
let src = lineage::source_table(opts);
let wd = opts.window_days;
let total_churn = window_total_churn(db, src, wd)?;
if total_churn <= 0.0 {
return Ok((0.0, 0.0)); }
let per_file = red_window_churn(db, src, wd)?;
if per_file.is_empty() {
return Ok((0.0, 0.0)); }
let window_start = window_start_rev(db, wd)?;
let head_fns = head_function_metrics(db, red_paths)?;
let empty_fns: FnMetrics = HashMap::new();
let mut improving_churn = 0.0_f64;
let mut red_churn = 0.0_f64;
for (path, churn) in &per_file {
red_churn += churn;
let head = head_fns.get(path).unwrap_or(&empty_fns);
let base = window_start
.as_deref()
.map_or_else(FnMetrics::new, |rev| base_function_metrics(repo, rev, path));
if file_improved(head, &base) {
improving_churn += churn;
}
}
let improving_pct = 100.0 * improving_churn / total_churn;
let degrading_pct = 100.0 * (red_churn - improving_churn) / total_churn;
Ok((improving_pct, degrading_pct))
}
#[cfg(test)]
mod tests {
use super::{EffortExposureRow, populate_bands_table};
use crate::analyses::code_health::CodeHealthRow;
use crate::quality_gates::evaluate_effort_exposure_rows;
use std::collections::HashMap;
#[test]
#[allow(clippy::too_many_lines)]
fn red_band_aggregation_path_is_covered() {
use crate::facts::FactsDb;
let db = FactsDb::new_in_memory().expect("in-memory db");
db.conn()
.execute(
"INSERT INTO commits (rev, author_email, author_name, \
committer_email, canonical_author, date, committer_date, \
message, is_merge, parent_count) \
VALUES ('abc1', 'a@b.com', 'A', 'a@b.com', 'A', \
TIMESTAMPTZ '2026-01-01', TIMESTAMPTZ '2026-01-01', \
'init', false, 1)",
[],
)
.expect("insert commit");
db.conn()
.execute(
"INSERT INTO changes (rev, path, change_type, loc_added, loc_deleted) \
VALUES ('abc1', 'src/bad.rs', 'modified', 80, 20), \
('abc1', 'src/good.rs', 'modified', 10, 5)",
[],
)
.expect("insert changes");
db.conn()
.execute(
"INSERT INTO complexity_metrics \
(path, name, rev, sloc) \
VALUES ('src/bad.rs', 'main', 'abc1', 500), \
('src/good.rs', 'main', 'abc1', 200), \
('src/ok.rs', 'main', 'abc1', 100)",
[],
)
.expect("insert complexity");
let health = vec![
CodeHealthRow {
path: "src/bad.rs".into(),
band: "red".into(),
cognitive: 42.0,
score: 20.0,
structural_risk: 0.9,
percentile: 0.95,
corpus_percentile: None,
beyond_corpus: false,
corpus_percentile_ci_low: None,
corpus_percentile_ci_high: None,
},
CodeHealthRow {
path: "src/ok.rs".into(),
band: "yellow".into(),
cognitive: 10.0,
score: 55.0,
structural_risk: 0.4,
percentile: 0.50,
corpus_percentile: None,
beyond_corpus: false,
corpus_percentile_ci_low: None,
corpus_percentile_ci_high: None,
},
CodeHealthRow {
path: "src/good.rs".into(),
band: "green".into(),
cognitive: 2.0,
score: 85.0,
structural_risk: 0.1,
percentile: 0.10,
corpus_percentile: None,
beyond_corpus: false,
corpus_percentile_ci_low: None,
corpus_percentile_ci_high: None,
},
];
let sloc_map: HashMap<String, i64> = HashMap::from([
("src/bad.rs".into(), 500),
("src/ok.rs".into(), 100),
("src/good.rs".into(), 200),
]);
populate_bands_table(&db, &health, &sloc_map).expect("populate_bands_table");
let sql = "
WITH win AS (
SELECT rev FROM commits
WHERE date >= (SELECT MAX(date) FROM commits) - INTERVAL '3650 days'
),
band_files AS (
SELECT band, COUNT(*) AS files,
COALESCE(SUM(sloc), 0) AS band_sloc
FROM eh_bands_v1 GROUP BY band
),
band_commits AS (
SELECT b.band, COUNT(DISTINCT c.rev) AS n_commits
FROM changes c
INNER JOIN win USING (rev)
INNER JOIN eh_bands_v1 b ON b.path = c.path
GROUP BY b.band
),
band_churn AS (
SELECT b.band,
COALESCE(SUM(c.loc_added + c.loc_deleted), 0) AS churn
FROM changes c
INNER JOIN win USING (rev)
INNER JOIN eh_bands_v1 b ON b.path = c.path
GROUP BY b.band
),
total_sloc AS (SELECT COALESCE(SUM(sloc), 0) AS v FROM eh_bands_v1),
total_commits AS (
SELECT COUNT(DISTINCT c.rev) AS v
FROM changes c
INNER JOIN win USING (rev)
INNER JOIN eh_bands_v1 b ON b.path = c.path
),
total_churn AS (
SELECT COALESCE(SUM(c.loc_added + c.loc_deleted), 0) AS v
FROM changes c
INNER JOIN win USING (rev)
INNER JOIN eh_bands_v1 b ON b.path = c.path
)
SELECT bf.band,
100.0 * COALESCE(bc.n_commits, 0)
/ NULLIF((SELECT v FROM total_commits), 0) AS commit_share_pct,
100.0 * COALESCE(bch.churn, 0)
/ NULLIF((SELECT v FROM total_churn), 0) AS churn_share_pct
FROM band_files bf
LEFT JOIN band_commits bc ON bc.band = bf.band
LEFT JOIN band_churn bch ON bch.band = bf.band
ORDER BY CASE bf.band WHEN 'red' THEN 1 WHEN 'yellow' THEN 2 ELSE 3 END
";
let mut stmt = db.conn().prepare(sql).expect("prepare agg sql");
let rows: Vec<(String, f64, f64)> = stmt
.query_map([], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, Option<f64>>(1)?.unwrap_or(0.0),
r.get::<_, Option<f64>>(2)?.unwrap_or(0.0),
))
})
.expect("query")
.collect::<Result<Vec<_>, _>>()
.expect("collect");
let bands: Vec<&str> = rows.iter().map(|(b, _, _)| b.as_str()).collect();
assert!(
bands.contains(&"red"),
"red band must be present; got {bands:?}"
);
assert!(bands.contains(&"yellow"), "yellow band must be present");
assert!(bands.contains(&"green"), "green band must be present");
let red = rows.iter().find(|(b, _, _)| b == "red").expect("red row");
assert!(
red.2 > 50.0,
"red churn_share_pct should be majority (≈87%): {}",
red.2
);
let yellow = rows
.iter()
.find(|(b, _, _)| b == "yellow")
.expect("yellow row");
assert!(
yellow.2.abs() < f64::EPSILON,
"yellow has no churn in window"
);
let red_churn_share = red.2;
let ee_rows: Vec<EffortExposureRow> = rows
.iter()
.map(|(b, commit_share_pct, churn_share_pct)| EffortExposureRow {
band: b.clone(),
files: 1,
loc_share_pct: 0.0,
commit_share_pct: *commit_share_pct,
churn_share_pct: *churn_share_pct,
commit_share_ci_low: 0.0,
commit_share_ci_high: 1.0,
churn_share_improving_pct: None,
churn_share_degrading_pct: None,
})
.collect();
let violations = evaluate_effort_exposure_rows(0.0, &ee_rows);
assert_eq!(
violations.len(),
1,
"threshold=0.0 must trigger one violation; got {violations:?}"
);
assert_eq!(
violations[0].gate, "max_red_effort_pct",
"gate name must match the threshold key"
);
let reported_actual: f64 = violations[0]
.actual
.parse()
.expect("actual field must be a parseable f64");
assert!(
(reported_actual - red_churn_share).abs() < 0.01,
"reported actual ({reported_actual:.2}) must match red churn share ({red_churn_share:.2})"
);
}
fn band_row(path: &str, band: &str) -> CodeHealthRow {
CodeHealthRow {
path: path.into(),
band: band.into(),
cognitive: 0.0,
score: 0.0,
structural_risk: 0.0,
percentile: 0.0,
corpus_percentile: None,
beyond_corpus: false,
corpus_percentile_ci_low: None,
corpus_percentile_ci_high: None,
}
}
#[test]
fn shares_exclude_churn_on_unbanded_paths() {
use super::run_effort_exposure_with_health;
use crate::Options;
use crate::facts::FactsDb;
let db = FactsDb::new_in_memory().expect("in-memory db");
db.conn()
.execute(
"INSERT INTO commits (rev, author_email, author_name, \
committer_email, canonical_author, date, committer_date, \
message, is_merge, parent_count) VALUES \
('c1','a@b.com','A','a@b.com','A',TIMESTAMPTZ '2026-01-01',TIMESTAMPTZ '2026-01-01','red',false,1), \
('c2','a@b.com','A','a@b.com','A',TIMESTAMPTZ '2026-01-01',TIMESTAMPTZ '2026-01-01','green',false,1), \
('c3','a@b.com','A','a@b.com','A',TIMESTAMPTZ '2026-01-01',TIMESTAMPTZ '2026-01-01','deps',false,1)",
[],
)
.expect("insert commits");
db.conn()
.execute(
"INSERT INTO changes (rev, path, change_type, loc_added, loc_deleted) VALUES \
('c1','src/bad.rs','modified',200,100), \
('c2','src/good.rs','modified',150,50), \
('c3','Cargo.lock','modified',3000,0), \
('c3','README.md','modified',1500,0)",
[],
)
.expect("insert changes");
db.conn()
.execute(
"INSERT INTO complexity_metrics (path, name, rev, sloc) VALUES \
('src/bad.rs','f','c1',500), \
('src/good.rs','f','c2',200)",
[],
)
.expect("insert complexity");
let health = vec![
band_row("src/bad.rs", "red"),
band_row("src/good.rs", "green"),
];
let opts = Options {
use_canonical_lineage: false,
..Options::default()
};
let rows = run_effort_exposure_with_health(&db, &opts, &health)
.expect("run effort-exposure with synthetic health");
let red = rows.iter().find(|r| r.band == "red").expect("red row");
let green = rows.iter().find(|r| r.band == "green").expect("green row");
assert!(
(red.churn_share_pct - 60.0).abs() < 1e-6,
"red churn share must be 60.0 (300 of 500 banded churn), got {}",
red.churn_share_pct
);
assert!(
(green.churn_share_pct - 40.0).abs() < 1e-6,
"green churn share must be 40.0, got {}",
green.churn_share_pct
);
assert!(
(red.churn_share_pct + green.churn_share_pct - 100.0).abs() < 1e-6,
"banded churn shares must sum to 100, got {}",
red.churn_share_pct + green.churn_share_pct
);
assert!(
(red.commit_share_pct - 50.0).abs() < 1e-6,
"red commit share must be 50.0 (1 of 2 banded commits), got {}",
red.commit_share_pct
);
assert!(
(green.commit_share_pct - 50.0).abs() < 1e-6,
"green commit share must be 50.0, got {}",
green.commit_share_pct
);
let red_share = red.commit_share_pct / 100.0;
assert!(
red.commit_share_ci_low <= red_share + 1e-9
&& red.commit_share_ci_high >= red_share - 1e-9,
"Wilson CI [{}, {}] must contain commit share {red_share}",
red.commit_share_ci_low,
red.commit_share_ci_high
);
let violations = evaluate_effort_exposure_rows(30.0, &rows);
assert_eq!(violations.len(), 1, "red 60 % must trip the 30 % ceiling");
assert_eq!(violations[0].gate, "max_red_effort_pct");
let reported: f64 = violations[0].actual.parse().expect("actual is f64");
assert!(
(reported - 60.0).abs() < 1e-6,
"violation must report the band-consistent 60.0, got {reported}"
);
}
use super::{FnMetrics, file_improved, net_movement};
fn fns(entries: &[(&str, u32, f64)]) -> FnMetrics {
entries
.iter()
.map(|&(n, loc, cyc)| (n.to_string(), (loc, cyc)))
.collect()
}
#[test]
fn net_movement_is_signed_weight_and_agrees_with_file_improved() {
let base = fns(&[("f", 120, 3.0)]);
let head = fns(&[("f", 10, 3.0)]);
assert!((net_movement(&head, &base) - 10.0).abs() < f64::EPSILON);
assert!(file_improved(&head, &base));
let base = fns(&[("f", 10, 3.0)]);
let head = fns(&[("f", 120, 3.0)]);
assert!((net_movement(&head, &base) + 120.0).abs() < f64::EPSILON);
assert!(!file_improved(&head, &base));
let base = fns(&[("f", 40, 3.0)]);
let head = fns(&[("f", 50, 3.0)]);
assert!(net_movement(&head, &base).abs() < f64::EPSILON);
let same = fns(&[("f", 40, 3.0)]);
assert!(net_movement(&same, &same.clone()).abs() < f64::EPSILON);
}
#[test]
fn file_improved_true_when_a_function_is_refactored_down() {
let base = fns(&[("f", 120, 3.0)]);
let head = fns(&[("f", 10, 3.0)]);
assert!(file_improved(&head, &base));
}
#[test]
fn file_improved_false_when_a_function_degrades() {
let base = fns(&[("f", 10, 3.0)]);
let head = fns(&[("f", 120, 3.0)]);
assert!(!file_improved(&head, &base));
}
#[test]
fn file_improved_false_on_neutral_only_change() {
let base = fns(&[("f", 40, 3.0)]);
let head = fns(&[("f", 50, 3.0)]);
assert!(!file_improved(&head, &base));
}
#[test]
fn file_improved_false_when_no_functions_changed() {
let same = fns(&[("f", 40, 3.0), ("g", 5, 1.0)]);
assert!(!file_improved(&same, &same.clone()));
}
#[test]
fn file_improved_added_high_risk_function_is_degrading() {
let base = FnMetrics::new();
let head = fns(&[("monster", 200, 20.0)]);
assert!(!file_improved(&head, &base));
}
#[test]
fn file_improved_removing_a_high_risk_function_is_good() {
let base = fns(&[("monster", 200, 20.0)]);
let head = FnMetrics::new();
assert!(file_improved(&head, &base));
}
#[test]
fn file_improved_net_of_concurrent_improve_and_degrade() {
let base = fns(&[("clean", 120, 3.0), ("rot", 10, 1.0)]);
let head = fns(&[("clean", 10, 3.0), ("rot", 200, 20.0)]);
assert!(!file_improved(&head, &base));
let base2 = fns(&[("clean", 200, 20.0), ("rot", 10, 1.0)]);
let head2 = fns(&[("rot", 40, 3.0)]);
assert!(file_improved(&head2, &base2));
}
}