use duckdb::params;
use crate::facts::FactsDb;
use crate::{Options, Result};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct HotspotRow {
pub path: String,
pub revisions: u32,
pub cognitive: f64,
pub cognitive_health: f64,
pub hotspot_score: f64,
pub mi: Option<f64>,
pub mi_rank: Option<f64>,
pub ai_pct: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hotspot_score_anchored: Option<f64>,
}
#[must_use]
pub fn build_sql(opts: &Options, cm_src: &str) -> String {
let file_mi_cte = if cm_src == "complexity_metrics_grouped" {
FILE_MI_GROUPED
} else {
FILE_MI_RAW
};
let ai_src = if opts.use_canonical_lineage {
"changes_lineage"
} else {
"changes"
};
let sql = SQL
.replace("{cm_src}", cm_src)
.replace("{file_mi_cte}", file_mi_cte);
crate::analyses::lineage::rewrite(&sql, opts).replace("{ai_src}", ai_src)
}
#[must_use]
pub fn build_inlined_sql(opts: &Options, cm_src: &str, min_revs: u32, row_limit: i64) -> String {
build_sql(opts, cm_src)
.replacen('?', &min_revs.to_string(), 1)
.replace('?', &row_limit.to_string())
}
const FILE_MI_RAW: &str = "file_mi AS (
SELECT
cm.path,
MAX(cm.mi) AS mi
FROM complexity_metrics cm
INNER JOIN entities e
ON e.path = cm.path
AND e.name = cm.name
AND e.rev_last_seen = cm.rev
WHERE e.kind = 'unit' AND cm.mi IS NOT NULL
GROUP BY cm.path
)";
const FILE_MI_GROUPED: &str = "file_mi AS (
SELECT path, mi
FROM complexity_metrics_grouped
WHERE mi IS NOT NULL
)";
pub const SQL: &str = "
WITH file_revs AS (
-- (rev, path) is the changes PK, so COUNT(rev) == COUNT(DISTINCT rev)
-- per path. Plain COUNT avoids DuckDB's distinct-tracking overhead.
SELECT path, COUNT(rev) AS revs
FROM changes
GROUP BY path
HAVING revs >= ?
),
file_complexity AS (
SELECT path, MAX(cognitive) AS cognitive
FROM {cm_src}
GROUP BY path
),
-- File-level MI body is swapped at build time depending on whether
-- grouping is active — see `FILE_MI_RAW` / `FILE_MI_GROUPED`. The
-- raw form joins `entities` to filter `kind='unit'`; the grouped
-- form reads pre-baked `mi` from `complexity_metrics_grouped`.
{file_mi_cte},
-- Per-file AI-attribution percentage: share of commits touching this
-- file that carry an AI-attribution signal (ai-assisted or
-- ai-authored). NULLIF guards against div-by-zero on the empty join
-- (defensive; file_revs already filters revs >= min_revs).
-- Categories come from identity::bots — schema is stable.
file_ai AS (
SELECT
ch.path,
COUNT(CASE WHEN co.ai_attribution IN ('ai-assisted', 'ai-authored')
THEN 1 END) * 100.0
/ NULLIF(COUNT(ch.rev), 0) AS ai_pct
FROM {ai_src} ch
INNER JOIN commits co ON co.rev = ch.rev
GROUP BY ch.path
),
-- Repo-relative MI percentile rank, computed ONLY over files that
-- actually have a `mi` value — files without a `kind='unit'` entry
-- (unsupported language / skipped file) MUST NOT skew the
-- distribution. PERCENT_RANK over file_mi emits values in [0, 1].
-- See analyses/mi.rs for why bands are repo-relative rather than
-- absolute Coleman/SEI thresholds (literature thresholds would
-- classify ~100% of any large codebase as 'low maintainability').
file_mi_ranked AS (
SELECT
path,
mi,
PERCENT_RANK() OVER (ORDER BY mi) AS mi_rank
FROM file_mi
),
joined AS (
SELECT
fr.path,
fr.revs,
COALESCE(fc.cognitive, 0) AS cognitive,
fmr.mi AS mi,
fmr.mi_rank AS mi_rank,
fa.ai_pct AS ai_pct
FROM file_revs fr
LEFT JOIN file_complexity fc ON fc.path = fr.path
LEFT JOIN file_mi_ranked fmr ON fmr.path = fr.path
LEFT JOIN file_ai fa ON fa.path = fr.path
),
ranked AS (
SELECT
path,
revs,
cognitive,
mi,
mi_rank,
ai_pct,
PERCENT_RANK() OVER (ORDER BY revs) AS pr_rev,
PERCENT_RANK() OVER (ORDER BY cognitive) AS pr_cx,
CASE
WHEN MAX(cognitive) OVER () > 0
THEN cognitive / MAX(cognitive) OVER ()
ELSE 0
END AS norm_cx
FROM joined
)
SELECT
path,
revs,
cognitive,
GREATEST(0.0, LEAST(100.0, 100.0 * (1.0 - 0.40 * norm_cx))) AS cognitive_health,
pr_rev * pr_cx * (100.0 - GREATEST(0.0, LEAST(100.0, 100.0 * (1.0 - 0.40 * norm_cx)))) / 4.0 AS score,
mi,
mi_rank,
ai_pct
FROM ranked
ORDER BY score DESC, path ASC
LIMIT ?
";
#[tracing::instrument(name = "hotspots", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_hotspots(db: &FactsDb, opts: &Options) -> Result<Vec<HotspotRow>> {
let row_limit: i64 = opts.rows_limit.map_or(i64::MAX, i64::from);
crate::analyses::lineage::materialize_source(db, opts)?;
let cm_src = crate::analyses::grouped_complexity::source_table(opts);
let sql = build_sql(opts, cm_src);
crate::analyses::query::explain_if_requested(
db,
&sql,
params![opts.min_revs, row_limit],
"hotspots",
opts,
)?;
crate::analyses::query::query_map_collect(
db,
&sql,
params![opts.min_revs, row_limit],
"hotspots",
|r| {
Ok(HotspotRow {
path: r.get::<_, String>(0)?,
revisions: u32::try_from(r.get::<_, i64>(1)?).unwrap_or(u32::MAX),
cognitive: r.get::<_, f64>(2)?,
cognitive_health: r.get::<_, f64>(3)?,
hotspot_score: r.get::<_, f64>(4)?,
mi: r.get::<_, Option<f64>>(5)?,
mi_rank: r.get::<_, Option<f64>>(6)?,
ai_pct: r.get::<_, Option<f64>>(7)?,
hotspot_score_anchored: None,
})
},
)
}
const PR_REV_SQL: &str = "
WITH file_revs AS (
SELECT path, COUNT(rev) AS revs
FROM changes
GROUP BY path
HAVING revs >= ?
)
SELECT path, PERCENT_RANK() OVER (ORDER BY revs) AS pr_rev
FROM file_revs
";
pub fn apply_hotspot_anchor(db: &FactsDb, opts: &Options, rows: &mut [HotspotRow]) -> Result<()> {
let Some(art) = crate::calibration::load_active_artifact(opts)? else {
return Ok(()); };
if rows.is_empty() {
return Ok(());
}
crate::analyses::lineage::materialize_source(db, opts)?;
let sql = crate::analyses::lineage::rewrite(PR_REV_SQL, opts);
let pr_rev_by_path: std::collections::HashMap<String, f64> =
crate::analyses::query::query_map_collect(
db,
&sql,
params![opts.min_revs],
"hotspot-pr-rev",
|r| Ok((r.get::<_, String>(0)?, r.get::<_, f64>(1)?)),
)?
.into_iter()
.collect();
for row in rows.iter_mut() {
let Some(language) = crate::complexity::Tier1Language::from_path(&row.path) else {
continue; };
let Some(cp_tail) = crate::calibration::conditional_tail_percentile(
&art,
language.as_str(),
"cognitive",
row.cognitive,
) else {
continue;
};
let Some(&pr_rev) = pr_rev_by_path.get(&row.path) else {
continue; };
row.hotspot_score_anchored = Some(anchored_score(pr_rev, cp_tail));
}
Ok(())
}
#[must_use]
fn anchored_score(pr_rev: f64, cp_tail: f64) -> f64 {
let health = (100.0 * (1.0 - 0.40 * cp_tail)).clamp(0.0, 100.0);
pr_rev * cp_tail * (100.0 - health) / 4.0
}
pub fn run_hotspots_anchored(db: &FactsDb, opts: &Options) -> Result<Vec<HotspotRow>> {
let mut rows = run_hotspots(db, opts)?;
apply_hotspot_anchor(db, opts, &mut rows)?;
Ok(rows)
}
#[cfg(test)]
mod anchor_tests {
use super::*;
use crate::calibration::{
CALIBRATION_FORMAT_VERSION, CalibrationArtifact, LanguageTable, MetricQuantiles,
QUANTILE_POINTS, Stratum,
};
use crate::facts::FactsDb;
#[test]
fn anchored_score_matches_closed_form() {
assert!((anchored_score(0.5, 0.9) - 4.05).abs() < 1e-12);
assert!(anchored_score(0.0, 1.0).abs() < 1e-12);
assert!(anchored_score(1.0, 0.0).abs() < 1e-12);
assert!((anchored_score(1.0, 1.0) - 10.0).abs() < 1e-12);
for &(pr, cp_tail) in &[(0.3, 0.7), (0.8, 0.25), (1.0, 0.997)] {
assert!((anchored_score(pr, cp_tail) - 10.0 * pr * cp_tail * cp_tail).abs() < 1e-12);
}
}
fn ramp_artifact(language: &str, sample_functions: u64) -> CalibrationArtifact {
#[allow(clippy::cast_precision_loss)] let quantiles: Vec<f64> = (0..QUANTILE_POINTS).map(|i| i as f64 / 100.0).collect();
CalibrationArtifact {
format_version: CALIBRATION_FORMAT_VERSION,
corpus_vintage: "test-ramp".into(),
generated_at: "2026-01-01T00:00:00Z".into(),
repos_included: 1,
repos_attempted: 1,
languages: vec![LanguageTable {
language: language.into(),
sample_functions,
strata: vec![Stratum {
sloc_min: 0,
sloc_max: u64::MAX,
metrics: vec![MetricQuantiles {
metric: "cognitive".into(),
quantiles,
}],
}],
}],
repo_metrics: None,
}
}
#[test]
fn anchored_score_reads_corpus_percentile() {
let art = ramp_artifact("rust", 1000);
let cp = crate::calibration::percentile(&art, "rust", "cognitive", 5.0)
.expect("rust clears the sample floor");
assert!(
(cp.p - 0.5).abs() < 1e-9,
"cognitive 5.0 is the ramp median"
);
let cp_tail =
crate::calibration::conditional_tail_percentile(&art, "rust", "cognitive", 5.0)
.expect("covered, non-empty tail");
assert!((cp_tail - 0.5).abs() < 1e-9, "p0 ≈ 0 ⇒ cp_tail == cp");
assert!((anchored_score(1.0, cp_tail) - 2.5).abs() < 1e-9);
}
#[test]
fn corpus_percentile_absent_below_sample_floor() {
let art = ramp_artifact("rust", 42);
assert!(crate::calibration::percentile(&art, "rust", "cognitive", 5.0).is_none());
}
fn seed(db: &FactsDb) {
for rev in ["c1", "c2", "c3"] {
db.conn()
.execute(
"INSERT INTO commits (rev, author_email, author_name, committer_email, \
canonical_author, date, committer_date, message, is_merge, parent_count) \
VALUES (?, 'a@b.com', 'A', 'a@b.com', 'A', TIMESTAMPTZ '2026-01-01', \
TIMESTAMPTZ '2026-01-01', 'm', false, 1)",
duckdb::params![rev],
)
.expect("insert commit");
}
db.conn()
.execute(
"INSERT INTO changes (rev, path, change_type) VALUES \
('c1','src/worst.rs','modified'),('c2','src/worst.rs','modified'),('c3','src/worst.rs','modified'), \
('c1','src/b.rs','modified'),('c2','src/b.rs','modified'), \
('c1','src/c.rs','modified')",
[],
)
.expect("insert changes");
db.conn()
.execute(
"INSERT INTO complexity_metrics (path, name, rev, cognitive) VALUES \
('src/worst.rs','f','c1',40),('src/b.rs','f','c1',20),('src/c.rs','f','c1',10)",
[],
)
.expect("insert complexity");
}
fn opts() -> Options {
Options {
repo_path: std::path::PathBuf::from("."),
min_revs: 1,
use_canonical_lineage: false,
..Options::default()
}
}
fn pr_rev_by_path(db: &FactsDb) -> std::collections::HashMap<String, f64> {
crate::analyses::query::query_map_collect(
db,
PR_REV_SQL,
duckdb::params![1u32],
"pr-rev-test",
|r| Ok((r.get::<_, String>(0)?, r.get::<_, f64>(1)?)),
)
.expect("pr_rev query")
.into_iter()
.collect()
}
#[test]
fn anchored_score_is_stable_when_worst_file_improves() {
let db = FactsDb::new_in_memory().expect("db");
seed(&db);
let opts = opts();
let art = crate::calibration::load_active_artifact(&opts)
.expect("load artifact")
.expect("embedded world corpus is active by default");
let cp_tail_b =
crate::calibration::conditional_tail_percentile(&art, "rust", "cognitive", 20.0)
.expect("src/b.rs is rust, covered above the sample floor");
assert!(cp_tail_b > 0.0, "the pin must be non-trivial");
let before = run_hotspots_anchored(&db, &opts).expect("run before");
let b_before = before.iter().find(|r| r.path == "src/b.rs").expect("b.rs");
let b_anchored_before = b_before
.hotspot_score_anchored
.expect("b.rs is rust, covered by the embedded corpus");
assert!((b_anchored_before - anchored_score(0.5, cp_tail_b)).abs() < 1e-9);
assert!((b_before.hotspot_score - 1.25).abs() < 1e-9);
db.conn()
.execute(
"UPDATE complexity_metrics SET cognitive = 5 WHERE path = 'src/worst.rs'",
[],
)
.expect("improve worst");
let after_inplace = run_hotspots_anchored(&db, &opts).expect("run after in-place");
let b_inplace = after_inplace
.iter()
.find(|r| r.path == "src/b.rs")
.expect("b.rs");
let b_anchored_inplace = b_inplace.hotspot_score_anchored.expect("still covered");
assert!(
(b_anchored_before - b_anchored_inplace).abs() < 1e-12,
"an in-place improvement must not move the anchored score \
(before {b_anchored_before}, after {b_anchored_inplace})"
);
assert!(
(b_inplace.hotspot_score - 5.0).abs() < 1e-9,
"legacy score MUST move (got {})",
b_inplace.hotspot_score
);
db.conn()
.execute(
"INSERT INTO commits (rev, author_email, author_name, committer_email, \
canonical_author, date, committer_date, message, is_merge, parent_count) \
VALUES ('c4', 'a@b.com', 'A', 'a@b.com', 'A', TIMESTAMPTZ '2026-01-01', \
TIMESTAMPTZ '2026-01-01', 'extract helper', false, 1)",
[],
)
.expect("insert refactor commit");
db.conn()
.execute(
"INSERT INTO changes (rev, path, change_type) \
VALUES ('c4', 'src/worst_helper.rs', 'added')",
[],
)
.expect("insert extracted file");
let after_shift = run_hotspots_anchored(&db, &opts).expect("run after shift");
let b_shift = after_shift
.iter()
.find(|r| r.path == "src/b.rs")
.expect("b.rs");
let b_anchored_shift = b_shift.hotspot_score_anchored.expect("still covered");
assert!(
(b_anchored_shift - anchored_score(2.0 / 3.0, cp_tail_b)).abs() < 1e-9,
"the anchored score tracks the repo-relative pr_rev (got {b_anchored_shift})"
);
assert!(
b_anchored_shift > b_anchored_before,
"an untouched file's anchored score DOES move when the revision \
population shifts — pr_rev is repo-relative by design \
(before {b_anchored_before}, after {b_anchored_shift})"
);
}
#[test]
fn anchored_score_untouched_file_moves_when_the_churn_population_shifts() {
let db = FactsDb::new_in_memory().expect("db");
let commit_rows: Vec<String> = (0..40)
.map(|i| {
format!(
"('r{i}', 'a@b.com', 'A', 'a@b.com', 'A', TIMESTAMPTZ '2026-01-01', \
TIMESTAMPTZ '2026-01-01', 'm', false, 1)"
)
})
.collect();
db.conn()
.execute(
&format!(
"INSERT INTO commits (rev, author_email, author_name, committer_email, \
canonical_author, date, committer_date, message, is_merge, parent_count) \
VALUES {}",
commit_rows.join(", ")
),
[],
)
.expect("insert 40 commits");
for (count, path) in [
(20, "src/b.rs"),
(29, "src/a.rs"),
(30, "src/c.rs"),
(40, "src/d.rs"),
] {
let change_rows: Vec<String> = (0..count)
.map(|i| format!("('r{i}', '{path}', 'modified')"))
.collect();
db.conn()
.execute(
&format!(
"INSERT INTO changes (rev, path, change_type) VALUES {}",
change_rows.join(", ")
),
[],
)
.expect("insert changes prefix");
}
let ramp = ramp_artifact("rust", 1000);
let cp_c = crate::calibration::conditional_tail_percentile(&ramp, "rust", "cognitive", 9.0)
.expect("rust covered, non-empty tail");
assert!(
(cp_c - 0.90).abs() < 1e-9,
"the ramp's tail lookup is the identity"
);
let pr_before = pr_rev_by_path(&db);
assert!(
(pr_before["src/c.rs"] - 2.0 / 3.0).abs() < 1e-9,
"src/c.rs starts at revisions rank 2/3 over {{20, 29, 30, 40}}"
);
let c_before = anchored_score(pr_before["src/c.rs"], cp_c);
assert!(
(c_before - 5.40).abs() < 1e-9,
"src/c.rs anchored 5.40 before the improving commit"
);
db.conn()
.execute(
"INSERT INTO changes (rev, path, change_type) VALUES ('r29', 'src/a.rs', 'modified')",
[],
)
.expect("src/a.rs gains a revision");
let pr_after = pr_rev_by_path(&db);
assert!(
(pr_after["src/c.rs"] - 1.0 / 3.0).abs() < 1e-9,
"src/a.rs caught up, so src/c.rs's rank drops to 1/3"
);
let c_after = anchored_score(pr_after["src/c.rs"], cp_c);
assert!(
(c_after - 2.70).abs() < 1e-9,
"src/c.rs anchored 2.70 after the commit, still untouched"
);
assert!(
c_before - c_after > 2.6,
"an untouched file's anchored score moved by ~2.70 \
(before {c_before}, after {c_after})"
);
}
#[test]
fn run_hotspots_never_populates_the_anchor() {
let db = FactsDb::new_in_memory().expect("db");
seed(&db);
let plain = run_hotspots(&db, &opts()).expect("plain run");
assert!(
plain.iter().all(|r| r.hotspot_score_anchored.is_none()),
"run_hotspots must leave the anchor absent"
);
}
}