use crate::CodeLoreError;
use crate::analyses::code_health::{HealthScanCtx, run_code_health_scoped};
use crate::analyses::knowledge::shares::materialize_knowledge_shares;
use crate::analyses::lineage;
use crate::facts::FactsDb;
use crate::options::Options;
use serde::Serialize;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize)]
pub struct MarginalOwnerRiskRow {
pub path: String,
pub band: String,
pub top_active_share: f64,
pub risk: String,
pub note: String,
}
#[must_use]
pub fn classify_risk(band: &str, top_active_share: f64) -> Option<&'static str> {
if band == "red" && top_active_share < 0.10 {
return Some("high");
}
if (band == "red" && top_active_share < 0.30) || (band == "yellow" && top_active_share < 0.10) {
return Some("elevated");
}
None
}
pub fn run_marginal_owner_risk(
db: &FactsDb,
opts: &Options,
) -> Result<Vec<MarginalOwnerRiskRow>, CodeLoreError> {
lineage::materialize_if_needed(db, opts)?;
materialize_knowledge_shares(db, opts)?;
let cx = HealthScanCtx::head_default();
let health_rows = run_code_health_scoped(db, opts, &cx)?;
let unhealthy: Vec<(String, String)> = health_rows
.into_iter()
.filter(|r| r.band == "yellow" || r.band == "red")
.map(|r| (r.path, r.band))
.collect();
if unhealthy.is_empty() {
return Ok(Vec::new());
}
let paths: Vec<String> = unhealthy.iter().map(|(path, _)| path.clone()).collect();
let top_active_shares = top_active_shares_by_path(db, opts, &paths)?;
let note = "changes here historically run 45-93% slower (ownership\u{00d7}health interaction)";
let mut rows: Vec<MarginalOwnerRiskRow> = Vec::new();
for (path, band) in &unhealthy {
let top_active_share = top_active_shares.get(path).copied().unwrap_or(0.0);
if let Some(risk) = classify_risk(band, top_active_share) {
rows.push(MarginalOwnerRiskRow {
path: path.clone(),
band: band.clone(),
top_active_share,
risk: risk.to_owned(),
note: note.to_owned(),
});
}
}
Ok(rows)
}
fn top_active_shares_by_path(
db: &FactsDb,
opts: &Options,
paths: &[String],
) -> Result<HashMap<String, f64>, CodeLoreError> {
if paths.is_empty() {
return Ok(HashMap::new());
}
let src = lineage::source_table(opts);
let placeholders = std::iter::repeat_n("(?)", paths.len())
.collect::<Vec<_>>()
.join(",");
let now_anchor = crate::analyses::query::clamped_now_anchor("date");
let sql = format!(
"WITH repo_end AS (
SELECT {now_anchor} AS end_date FROM commits
),
active_authors AS (
SELECT DISTINCT co.canonical_author
FROM {src} ch
JOIN commits co ON co.rev = ch.rev
JOIN repo_end re ON TRUE
WHERE co.date >= re.end_date - INTERVAL '{window}' DAY
),
requested_paths(path) AS (
VALUES {placeholders}
),
path_shares AS (
SELECT rp.path, ks.k_norm
FROM requested_paths rp
JOIN knowledge_shares ks ON ks.path = rp.path
JOIN active_authors aa ON aa.canonical_author = ks.author
)
SELECT rp.path, COALESCE(MAX(ps.k_norm), 0.0) AS top_active_share
FROM requested_paths rp
LEFT JOIN path_shares ps ON ps.path = rp.path
GROUP BY rp.path",
src = src,
window = opts.window_days,
);
let mut stmt = db
.conn()
.prepare(&sql)
.map_err(|e| CodeLoreError::Analysis(format!("marginal_owner_risk prepare: {e}")))?;
let params: Vec<&dyn duckdb::ToSql> = paths.iter().map(|p| p as &dyn duckdb::ToSql).collect();
stmt.query_map(params.as_slice(), |r| {
Ok((r.get::<_, String>(0)?, r.get::<_, f64>(1)?))
})
.map_err(|e| CodeLoreError::Analysis(format!("marginal_owner_risk query: {e}")))?
.collect::<std::result::Result<HashMap<_, _>, _>>()
.map_err(|e| CodeLoreError::Analysis(format!("marginal_owner_risk collect: {e}")))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::facts::FactsDb;
#[test]
fn top_active_shares_by_path_does_not_cross_paths() {
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', 'alice@x.com', 'Alice', 'alice@x.com', 'Alice', \
TIMESTAMP '2024-01-01', TIMESTAMP '2024-01-01', 'a', false, 1), \
('c2', 'bob@x.com', 'Bob', 'bob@x.com', 'Bob', \
TIMESTAMP '2024-05-01', TIMESTAMP '2024-05-01', 'b', false, 1), \
('c3', 'carol@x.com', 'Carol', 'carol@x.com', 'Carol', \
TIMESTAMP '2024-04-20', TIMESTAMP '2024-04-20', 'c', false, 1)",
[],
)
.expect("insert commits");
db.conn()
.execute(
"INSERT INTO changes (rev, path, change_type, loc_added, loc_deleted) \
VALUES ('c1', 'pathA.rs', 'modified', 1, 0), \
('c2', 'pathB.rs', 'modified', 1, 0), \
('c3', 'pathC.rs', 'modified', 1, 0)",
[],
)
.expect("insert changes");
db.conn()
.execute_batch(
"CREATE TEMP TABLE knowledge_shares \
(path TEXT, author TEXT, k DOUBLE, k_norm DOUBLE)",
)
.expect("create knowledge_shares");
db.conn()
.execute(
"INSERT INTO knowledge_shares (path, author, k, k_norm) VALUES \
('pathA.rs', 'Alice', 1.0, 1.0), \
('pathB.rs', 'Bob', 0.8, 0.8), \
('pathB.rs', 'Carol', 0.2, 0.2), \
('pathC.rs', 'Alice', 0.9, 0.9), \
('pathC.rs', 'Carol', 0.1, 0.1), \
('pathD.rs', 'Bob', 1.0, 1.0)",
[],
)
.expect("insert knowledge_shares");
let opts = Options {
use_canonical_lineage: false,
..Options::default()
};
let paths = vec![
"pathA.rs".to_owned(),
"pathB.rs".to_owned(),
"pathC.rs".to_owned(),
];
let shares = top_active_shares_by_path(&db, &opts, &paths).expect("batched query");
assert_eq!(shares.get("pathA.rs").copied(), Some(0.0));
assert_eq!(shares.get("pathB.rs").copied(), Some(0.8));
assert_eq!(shares.get("pathC.rs").copied(), Some(0.1));
assert_eq!(shares.get("pathD.rs"), None);
assert_eq!(shares.len(), 3);
}
#[test]
fn top_active_shares_by_path_empty_paths_returns_empty_map_without_querying() {
let db = FactsDb::new_in_memory().expect("in-memory db");
let opts = Options::default();
let shares = top_active_shares_by_path(&db, &opts, &[]).expect("empty paths");
assert!(shares.is_empty());
}
}