use std::collections::HashMap;
use crate::analyses::code_health::{HealthScanCtx, run_code_health_scoped};
use crate::analyses::knowledge::shares::materialize_knowledge_shares;
use crate::analyses::lineage::source_table;
use crate::facts::FactsDb;
use crate::{CodeLoreError, Options, Result};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CoordinationNeedsRow {
pub path: String,
pub authors: u32,
pub fragmentation: f64,
pub interleave: f64,
pub cochange_entropy: f64,
pub tier: String,
pub health_band: String,
pub total_commits: u32,
}
#[allow(clippy::too_many_lines)]
pub fn run_coordination_needs(db: &FactsDb, opts: &Options) -> Result<Vec<CoordinationNeedsRow>> {
materialize_knowledge_shares(db, opts)?;
let src = source_table(opts);
let now_anchor = crate::analyses::query::clamped_now_anchor("date");
let frag_sql = format!(
"WITH window_cutoff AS (
SELECT {now_anchor} - INTERVAL ({wd}) DAY AS cutoff FROM commits
),
active AS (
SELECT DISTINCT ch.path, co.canonical_author
FROM {src} ch
JOIN commits co ON co.rev = ch.rev
CROSS JOIN window_cutoff
WHERE co.date >= window_cutoff.cutoff
),
frag AS (
-- COALESCE guards a path whose decayed knowledge sums to zero
-- (e.g. binary files / deletion-only history, where every
-- contributor's loc_added is 0): k_norm is NULL for every row
-- of that path (0 / NULLIF(0, 0) = 0 / NULL), so SUM(k_norm²)
-- is NULL and would otherwise propagate a NULL fragmentation —
-- mirrors knowledge_islands's `HAVING SUM(loc) > 0` defensive
-- intent, applied here as a COALESCE since the degenerate case
-- must still emit a row (zero fragmentation), not be dropped.
SELECT
ks.path,
COALESCE(1.0 - SUM(ks.k_norm * ks.k_norm), 0.0) AS fragmentation
FROM knowledge_shares ks
GROUP BY ks.path
),
act_count AS (
SELECT path, COUNT(DISTINCT canonical_author) AS active_authors
FROM active
GROUP BY path
)
SELECT frag.path,
COALESCE(act_count.active_authors, 0) AS authors,
frag.fragmentation
FROM frag
LEFT JOIN act_count ON act_count.path = frag.path
WHERE frag.path IS NOT NULL
ORDER BY frag.fragmentation DESC, frag.path ASC",
src = src,
wd = opts.window_days,
);
let mut frag_stmt = db
.conn()
.prepare(&frag_sql)
.map_err(|e| CodeLoreError::Analysis(format!("prepare fragmentation query: {e}")))?;
let frag_rows = frag_stmt
.query_map([], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, u32>(1)?,
r.get::<_, f64>(2)?,
))
})
.map_err(|e| CodeLoreError::Analysis(format!("query fragmentation: {e}")))?
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| CodeLoreError::Analysis(format!("collect fragmentation: {e}")))?;
let interleave_sql = format!(
"WITH ordered AS (
SELECT
ch.path,
co.canonical_author AS cur,
LAG(co.canonical_author) OVER (
PARTITION BY ch.path ORDER BY co.date, co.rowid
) AS prev
FROM {src} ch
JOIN commits co ON co.rev = ch.rev
),
stats AS (
SELECT
path,
COUNT(*) AS n_commits,
COUNT(*) FILTER (WHERE prev IS NOT NULL AND prev != cur) AS switches
FROM ordered
GROUP BY path
)
SELECT path,
CASE WHEN n_commits < 2 THEN 0.0
ELSE switches * 1.0 / (n_commits - 1)
END AS interleave,
n_commits
FROM stats",
);
let mut il_stmt = db
.conn()
.prepare(&interleave_sql)
.map_err(|e| CodeLoreError::Analysis(format!("prepare interleave query: {e}")))?;
let interleave_map: HashMap<String, (f64, u32)> = il_stmt
.query_map([], |r| {
Ok((
r.get::<_, String>(0)?,
(r.get::<_, f64>(1)?, r.get::<_, u32>(2)?),
))
})
.map_err(|e| CodeLoreError::Analysis(format!("query interleave: {e}")))?
.collect::<std::result::Result<HashMap<_, _>, _>>()
.map_err(|e| CodeLoreError::Analysis(format!("collect interleave: {e}")))?;
let entropy_sql = format!(
"WITH win AS (
SELECT co.rev FROM commits co
WHERE co.nf <= 30
AND co.date >= (SELECT {now_anchor} FROM commits) - INTERVAL ({wd}) DAY
),
edges AS (
SELECT DISTINCT
LEAST(a.path, b.path) AS p1,
GREATEST(a.path, b.path) AS p2
FROM {src} a
JOIN {src} b ON a.rev = b.rev AND a.path < b.path
JOIN win w ON w.rev = a.rev
),
deg AS (
SELECT path, COUNT(*) AS d
FROM (
SELECT p1 AS path FROM edges
UNION ALL
SELECT p2 AS path FROM edges
)
GROUP BY path
),
tot AS (SELECT SUM(d) AS twoe FROM deg),
p AS (
SELECT path, d * 1.0 / twoe AS pk
FROM deg, tot
WHERE twoe > 0
),
h AS (
-- ORDER BY path forces DuckDB to sum the entropy terms in a
-- fixed (total-ordered, since path is unique in `p`) sequence
-- instead of whatever order parallel partitioning happens to
-- produce. Float addition is non-associative, so the unordered
-- SUM wobbles by ~1 ULP run to run and defeats byte-for-byte
-- reproducibility; the ordered aggregate pins one value. Cheap:
-- coordination-needs is not a hot path and `p` has one row per
-- file. `twoe = SUM(d)` above is exact integer arithmetic, so
-- only this float SUM needs pinning.
SELECT -SUM(pk * LN(pk) ORDER BY path) AS hs FROM p WHERE pk > 0
)
SELECT p.path, p.pk * h.hs AS h_a FROM p, h",
src = src,
wd = opts.window_days,
);
let mut ent_stmt = db
.conn()
.prepare(&entropy_sql)
.map_err(|e| CodeLoreError::Analysis(format!("prepare entropy query: {e}")))?;
let entropy_map: HashMap<String, f64> = ent_stmt
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, f64>(1)?)))
.map_err(|e| CodeLoreError::Analysis(format!("query entropy: {e}")))?
.collect::<std::result::Result<HashMap<_, _>, _>>()
.map_err(|e| CodeLoreError::Analysis(format!("collect entropy: {e}")))?;
let ctx = HealthScanCtx::head_default();
let health_rows = run_code_health_scoped(db, opts, &ctx)?;
let band_map: HashMap<String, String> =
health_rows.into_iter().map(|r| (r.path, r.band)).collect();
let mut rows: Vec<CoordinationNeedsRow> = frag_rows
.into_iter()
.map(|(path, authors, fragmentation)| {
let (interleave, total_commits) =
interleave_map.get(&path).copied().unwrap_or((0.0, 0));
let cochange_entropy = entropy_map.get(&path).copied().unwrap_or(0.0);
let health_band = band_map
.get(&path)
.cloned()
.unwrap_or_else(|| "unknown".to_string());
let tier = classify_tier(authors, fragmentation, interleave);
CoordinationNeedsRow {
path,
authors,
fragmentation,
interleave,
cochange_entropy,
tier,
health_band,
total_commits,
}
})
.collect();
rows.sort_by(|a, b| {
b.fragmentation
.partial_cmp(&a.fragmentation)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.path.cmp(&b.path))
});
Ok(rows)
}
fn classify_tier(authors: u32, fragmentation: f64, interleave: f64) -> String {
if authors <= 1 {
return "single".to_string();
}
if fragmentation < 0.25 {
return "low".to_string();
}
if fragmentation >= 0.50 && interleave >= 0.50 {
return "high".to_string();
}
"medium".to_string()
}