use std::collections::HashMap;
use duckdb::params;
use crate::analyses::coupling::{run_coupling, run_coupling_scoped};
use crate::facts::FactsDb;
use crate::{CodeLoreError, Options, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CloneSource {
WorkingTree,
Head,
}
#[derive(Debug, Clone)]
pub struct HealthScanCtx {
pub complexity_source: String,
pub imports_source: String,
pub history_cutoff: Option<String>,
pub include_clones: bool,
pub clone_source: CloneSource,
}
impl HealthScanCtx {
#[must_use]
pub fn head() -> Self {
Self {
complexity_source: "complexity_metrics".to_string(),
imports_source: "imports".to_string(),
history_cutoff: None,
include_clones: true,
clone_source: CloneSource::WorkingTree,
}
}
#[must_use]
pub fn head_default() -> Self {
Self::head()
}
}
pub(crate) const SMELL_WEIGHTS: &[(&str, f64)] = &[
("complex-method", 0.22),
("god-class", 0.18),
("large-method", 0.12),
("dry", 0.12),
("shotgun-surgery", 0.12),
("deep-nesting", 0.10),
("many-args", 0.07),
("complex-conditional", 0.07),
];
const STRUCTURAL_SCALE_NO_DRY: &str = " / 0.88";
const MIN_COHORT_FILES: usize = 10;
pub(crate) fn default_smell_weights() -> Vec<(String, f64)> {
SMELL_WEIGHTS
.iter()
.map(|&(name, w)| (name.to_string(), w))
.collect()
}
fn smell_weights_case(weights: &[(String, f64)]) -> String {
use std::fmt::Write as _;
let mut case = String::from("CASE smell");
for (smell, weight) in weights {
let _ = write!(case, "\n WHEN '{smell}' THEN {weight}");
}
case.push_str("\n ELSE 0.0\n END");
case
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct CodeHealthRow {
pub path: String,
pub cognitive: f64,
pub score: f64, pub structural_risk: f64, pub percentile: f64, pub band: String, #[serde(skip_serializing_if = "Option::is_none")]
pub corpus_percentile: Option<f64>,
#[serde(skip_serializing_if = "std::ops::Not::not", default)]
pub beyond_corpus: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub corpus_percentile_ci_low: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub corpus_percentile_ci_high: Option<f64>,
}
const SQL: &str = "
WITH file_cognitive AS (
SELECT path, MAX(cognitive) AS cognitive
FROM {cm_src}
GROUP BY path
),
file_churn AS (
SELECT path, COALESCE(SUM(loc_added), 0) + COALESCE(SUM(loc_deleted), 0) AS churn
FROM {src}
GROUP BY path
),
file_revs AS (
-- (rev, path) is the changes PK; COUNT(rev) == COUNT(DISTINCT rev)
-- per path. Plain COUNT skips DuckDB's distinct-tracking overhead.
SELECT path, COUNT(rev) AS revs
FROM {src}
GROUP BY path
HAVING revs >= ?
),
author_revs AS (
SELECT
c.path,
commits.canonical_author AS author,
-- (rev, path) is the changes PK so rev is unique within each
-- (path, author) group. Plain COUNT skips DuckDB's
-- distinct-tracking overhead.
COUNT(c.rev) AS revs
FROM {src} c
INNER JOIN commits ON c.rev = commits.rev
GROUP BY c.path, commits.canonical_author
),
file_fv AS (
SELECT
ar.path,
1.0 - SUM(POWER(ar.revs::DOUBLE / NULLIF(t.total, 0), 2)) AS fv
FROM author_revs ar
INNER JOIN (SELECT path, SUM(revs) AS total FROM author_revs GROUP BY path) t
ON ar.path = t.path
GROUP BY ar.path
),
-- Weighted sum of per-file biomarker intensities (each a per-language
-- PERCENT_RANK in [0,1]; smells absent for a file contribute 0). The
-- weights sum to 1.0 and are ordered by empirical defect-correlation
-- strength, so structural risk stays in [0,1] and spreads across the file
-- distribution instead of saturating at the ceiling. Co-occurrence is
-- implicit: a file flagged by more smells accumulates more weighted terms.
file_structural AS (
SELECT
path,
LEAST(1.0, SUM(intensity * {smell_weights_case}){structural_scale}) AS structural_risk
FROM code_health_biomarkers_v1
GROUP BY path
),
-- Coupling centrality enters the composite once, as the shotgun-surgery
-- biomarker inside structural_risk. It is deliberately NOT also added as a
-- separate behavioral term here — that would double-count the same signal.
joined AS (
SELECT
fc.path,
fc.cognitive,
COALESCE(fch.churn, 0) AS churn,
COALESCE(ffv.fv, 0.0) AS fv,
COALESCE(fs.structural_risk, 0.0) AS structural_risk
FROM file_cognitive fc
INNER JOIN file_revs fr ON fc.path = fr.path
LEFT JOIN file_churn fch ON fc.path = fch.path
LEFT JOIN file_fv ffv ON fc.path = ffv.path
LEFT JOIN file_structural fs ON fc.path = fs.path
),
normalized AS (
SELECT
path,
cognitive,
churn,
fv,
structural_risk,
CASE WHEN MAX(churn) OVER () > 0 THEN churn::DOUBLE / MAX(churn) OVER () ELSE 0 END AS n_cn,
fv AS n_au
FROM joined
),
scored AS (
SELECT
path,
cognitive,
structural_risk,
GREATEST(0.0, LEAST(100.0,
100.0 * (1.0 - 0.50 * structural_risk - 0.30 * n_cn - 0.20 * n_au)
)) AS score,
CASE lower(split_part(path, '.', -1))
WHEN 'rs' THEN 'rust'
WHEN 'py' THEN 'python' WHEN 'pyi' THEN 'python'
WHEN 'java' THEN 'java'
WHEN 'js' THEN 'javascript' WHEN 'jsx' THEN 'javascript'
WHEN 'mjs' THEN 'javascript' WHEN 'cjs' THEN 'javascript'
WHEN 'ts' THEN 'typescript' WHEN 'tsx' THEN 'typescript'
ELSE 'other'
END AS lang
FROM normalized
)
SELECT
path,
cognitive,
score,
structural_risk,
PERCENT_RANK() OVER (PARTITION BY lang ORDER BY structural_risk) AS percentile,
-- Absolute structural_risk thresholds. Deliberately self-relative:
-- the corpus-relative lens is an ADDITIVE second reading and never
-- moves these bands. red = high on a majority of the weighted
-- smell mass.
CASE
WHEN structural_risk >= {risk_red_min} THEN 'red'
WHEN structural_risk >= {risk_yellow_min} THEN 'yellow'
ELSE 'green'
END AS band
FROM scored
ORDER BY score ASC, path ASC
LIMIT ?
";
const CHANGES_AT_TS_DDL: &str = "
CREATE OR REPLACE TEMPORARY VIEW changes_at_ts AS
SELECT c.* FROM changes c
INNER JOIN commits ON commits.rev = c.rev
WHERE commits.date <= CAST('{ts}' AS TIMESTAMP)
";
const CENTRALITY_DDL: &str = "
CREATE OR REPLACE TEMPORARY TABLE coupling_centrality_v1 (
path TEXT PRIMARY KEY,
centrality INTEGER NOT NULL
)
";
const BIOMARKERS_DDL: &str = "
CREATE OR REPLACE TEMPORARY TABLE code_health_biomarkers_v1 (
path TEXT NOT NULL,
smell TEXT NOT NULL,
intensity DOUBLE NOT NULL
)
";
const BIOMARKERS_INSERT: &str = "
INSERT INTO code_health_biomarkers_v1 (path, smell, intensity)
WITH lang_fn AS (
SELECT
path,
name,
cyclomatic,
loc,
max_nesting,
nargs,
bool_ops,
CASE lower(split_part(path, '.', -1))
WHEN 'rs' THEN 'rust'
WHEN 'py' THEN 'python'
WHEN 'pyi' THEN 'python'
WHEN 'java' THEN 'java'
WHEN 'js' THEN 'javascript'
WHEN 'jsx' THEN 'javascript'
WHEN 'mjs' THEN 'javascript'
WHEN 'cjs' THEN 'javascript'
WHEN 'ts' THEN 'typescript'
WHEN 'tsx' THEN 'typescript'
ELSE 'other'
END AS lang
FROM {cm_src}
WHERE cyclomatic IS NOT NULL
AND loc IS NOT NULL
),
-- Aggregate to the file FIRST (worst function per file), THEN rank files.
-- Ranking functions and taking the per-file MAX saturates: any file with
-- enough functions has one in the top percentile, so nearly every file
-- scored ~1.0. Ranking files against files spreads the intensity uniformly.
file_metric AS (
SELECT
path,
lang,
MAX(cyclomatic) AS file_cx,
MAX(loc) AS file_loc,
MAX(max_nesting) AS file_nesting,
MAX(nargs) AS file_nargs,
MAX(bool_ops) AS file_bool_ops
FROM lang_fn
GROUP BY path, lang
),
ranked AS (
SELECT
path,
COUNT(*) OVER (PARTITION BY lang) AS lang_n,
PERCENT_RANK() OVER (PARTITION BY lang ORDER BY file_cx) AS cx_i,
PERCENT_RANK() OVER (PARTITION BY lang ORDER BY file_loc) AS loc_i,
PERCENT_RANK() OVER (PARTITION BY lang ORDER BY file_nesting) AS nesting_i,
PERCENT_RANK() OVER (PARTITION BY lang ORDER BY file_nargs) AS nargs_i,
PERCENT_RANK() OVER (PARTITION BY lang ORDER BY file_bool_ops) AS bool_ops_i
FROM file_metric
),
-- A per-language cohort thinner than the file floor is too small for a
-- percent-rank to carry meaning: at two files PERCENT_RANK emits exactly
-- {0.0, 1.0}, pinning the worse of the pair to MAX intensity on every
-- biomarker regardless of its absolute complexity. Drop below-floor cohorts
-- entirely — they yield no structural-biomarker rows — mirroring the corpus
-- lens, which treats a language pooled below its sample floor as absent
-- rather than speaking from too thin a sample. The floor is bound from the
-- MIN_COHORT_FILES constant.
floored AS (
SELECT * FROM ranked WHERE lang_n >= {min_cohort_files}
)
SELECT path, 'complex-method' AS smell, cx_i AS intensity FROM floored
UNION ALL
SELECT path, 'large-method' AS smell, loc_i AS intensity FROM floored
UNION ALL
SELECT path, 'deep-nesting' AS smell, nesting_i AS intensity FROM floored
UNION ALL
SELECT path, 'many-args' AS smell, nargs_i AS intensity FROM floored
UNION ALL
SELECT path, 'complex-conditional' AS smell, bool_ops_i AS intensity FROM floored
";
const SHOTGUN_INSERT: &str = "
INSERT INTO code_health_biomarkers_v1 (path, smell, intensity)
SELECT path, 'shotgun-surgery' AS smell,
PERCENT_RANK() OVER (ORDER BY centrality) AS intensity
FROM coupling_centrality_v1
WHERE centrality > 0
";
fn materialize_biomarkers(db: &FactsDb, opts: &Options, cx: &HealthScanCtx) -> Result<()> {
db.conn()
.execute(BIOMARKERS_DDL, [])
.map_err(|e| CodeLoreError::Analysis(format!("create biomarker temp table: {e}")))?;
let biomarkers_insert = BIOMARKERS_INSERT
.replace("{cm_src}", &cx.complexity_source)
.replace("{min_cohort_files}", &MIN_COHORT_FILES.to_string());
db.conn()
.execute(&biomarkers_insert, [])
.map_err(|e| CodeLoreError::Analysis(format!("insert complexity biomarkers: {e}")))?;
db.conn()
.execute(SHOTGUN_INSERT, [])
.map_err(|e| CodeLoreError::Analysis(format!("insert shotgun-surgery biomarkers: {e}")))?;
let gods = crate::analyses::god_classes::run_god_classes_scoped(
db,
&opts.with_no_row_limit(),
&cx.complexity_source,
&cx.imports_source,
)?;
let god_by_path: HashMap<String, f64> =
gods.iter().map(|g| (g.path.clone(), g.god_score)).collect();
let dry_counts: HashMap<String, u32> = match (cx.include_clones, cx.clone_source) {
(false, _) => HashMap::new(),
(true, CloneSource::WorkingTree) => {
let clones = crate::analyses::clones::run_clones_memoised(db, opts)?;
let mut m: HashMap<String, u32> = HashMap::new();
for c in clones.iter() {
*m.entry(c.entity.clone()).or_insert(0) += 1;
}
m
}
(true, CloneSource::Head) => crate::analyses::clones::head_clone_counts(db)?,
};
let universe_sql = "SELECT DISTINCT path FROM {cm_src} \
WHERE cyclomatic IS NOT NULL AND loc IS NOT NULL"
.replace("{cm_src}", &cx.complexity_source);
let universe = crate::analyses::query::query_map_collect(
db,
&universe_sql,
[],
"biomarker-universe",
|r| r.get::<_, String>(0),
)?;
let mut by_lang: HashMap<&'static str, Vec<String>> = HashMap::new();
for path in universe {
let lang =
crate::complexity::Tier1Language::from_path(&path).map_or("other", |l| l.as_str());
by_lang.entry(lang).or_default().push(path);
}
let mut stmt = db
.conn()
.prepare("INSERT INTO code_health_biomarkers_v1 (path, smell, intensity) VALUES (?, ?, ?)")
.map_err(|e| CodeLoreError::Analysis(format!("prepare biomarker insert: {e}")))?;
for files in by_lang.values() {
if files.len() < MIN_COHORT_FILES {
continue; }
let denom = f64::from(u32::try_from(files.len() - 1).unwrap_or(u32::MAX));
let smells: &[&str] = if cx.include_clones {
&["god-class", "dry"]
} else {
&["god-class"]
};
for &smell in smells {
let vals: Vec<f64> = files
.iter()
.map(|p| {
if smell == "god-class" {
god_by_path.get(p).copied().unwrap_or(0.0)
} else {
dry_counts.get(p).map_or(0.0, |n| f64::from(*n))
}
})
.collect();
for (path, &v) in files.iter().zip(vals.iter()) {
if v <= 0.0 {
continue; }
let less = vals.iter().filter(|&&x| x < v).count();
let intensity = f64::from(u32::try_from(less).unwrap_or(u32::MAX)) / denom;
stmt.execute(params![path, smell, intensity])
.map_err(|e| CodeLoreError::Analysis(format!("{smell} biomarker: {e}")))?;
}
}
}
Ok(())
}
fn materialize_centrality(db: &FactsDb, opts: &Options, cx: &HealthScanCtx) -> Result<()> {
let pairs = if cx.history_cutoff.is_some() {
run_coupling_scoped(db, &opts.with_no_row_limit(), "changes_at_ts")?
} else {
run_coupling(db, &opts.with_no_row_limit())?
};
let mut counts: HashMap<String, u32> = HashMap::new();
for p in &pairs {
*counts.entry(p.entity_a.clone()).or_insert(0) += 1;
*counts.entry(p.entity_b.clone()).or_insert(0) += 1;
}
db.conn()
.execute(CENTRALITY_DDL, [])
.map_err(|e| CodeLoreError::Analysis(format!("create centrality temp table: {e}")))?;
if counts.is_empty() {
return Ok(()); }
let mut stmt = db
.conn()
.prepare("INSERT INTO coupling_centrality_v1 (path, centrality) VALUES (?, ?)")
.map_err(|e| CodeLoreError::Analysis(format!("prepare centrality insert: {e}")))?;
for (path, count) in &counts {
stmt.execute(params![path, *count])
.map_err(|e| CodeLoreError::Analysis(format!("centrality row insert: {e}")))?;
}
Ok(())
}
const CORPUS_METRICS: &[&str] = &["cyclomatic", "cognitive", "sloc", "nargs", "max_nesting"];
#[allow(clippy::cast_precision_loss)]
fn metric_to_f64(n: i64) -> f64 {
n as f64
}
const FILE_AGGREGATES_SQL: &str = "
SELECT
path,
MAX(cyclomatic) AS cyclomatic,
MAX(cognitive) AS cognitive,
MAX(sloc) AS sloc,
MAX(nargs) AS nargs,
MAX(max_nesting) AS max_nesting
FROM {cm_src}
GROUP BY path
";
fn file_corpus_lens(
art: &crate::calibration::CalibrationArtifact,
language: &str,
metrics: &[(&str, Option<f64>)],
) -> Option<(f64, bool)> {
let mut worst: Option<f64> = None;
let mut beyond = false;
for &(metric, value) in metrics {
let Some(value) = value else { continue };
if let Some(cp) = crate::calibration::percentile(art, language, metric, value) {
worst = Some(worst.map_or(cp.p, |w: f64| w.max(cp.p)));
beyond |= cp.beyond_corpus;
}
}
worst.map(|p| (p, beyond))
}
fn apply_corpus_lens(
db: &FactsDb,
opts: &Options,
cx: &HealthScanCtx,
rows: &mut [CodeHealthRow],
) -> Result<()> {
let Some(art) = crate::calibration::load_active_artifact(opts)? else {
return Ok(()); };
let aggregates_sql = FILE_AGGREGATES_SQL.replace("{cm_src}", &cx.complexity_source);
let mut stmt = db
.conn()
.prepare(&aggregates_sql)
.map_err(|e| CodeLoreError::Analysis(format!("prepare corpus aggregates: {e}")))?;
let mut by_path: HashMap<String, [Option<f64>; 5]> = HashMap::new();
let aggregate_rows = stmt
.query_map([], |r| {
let path = r.get::<_, String>(0)?;
let mut vals = [None; 5];
for (i, slot) in vals.iter_mut().enumerate() {
*slot = r.get::<_, Option<i64>>(i + 1)?.map(metric_to_f64);
}
Ok((path, vals))
})
.map_err(|e| CodeLoreError::Analysis(format!("query corpus aggregates: {e}")))?;
for row in aggregate_rows {
let (path, vals) =
row.map_err(|e| CodeLoreError::Analysis(format!("collect corpus aggregates: {e}")))?;
by_path.insert(path, vals);
}
for row in rows.iter_mut() {
let Some(language) = crate::complexity::Tier1Language::from_path(&row.path) else {
continue; };
let Some(vals) = by_path.get(&row.path) else {
continue; };
let metrics: Vec<(&str, Option<f64>)> = CORPUS_METRICS
.iter()
.zip(vals.iter())
.map(|(&m, &v)| (m, v))
.collect();
if let Some((p, beyond)) = file_corpus_lens(&art, language.as_str(), &metrics) {
row.corpus_percentile = Some(p);
row.beyond_corpus = beyond;
if let Some(n) = crate::calibration::language_sample_functions(&art, language.as_str())
{
let (lo, hi) = crate::stats::wilson_ci_from_proportion(
p,
u32::try_from(n).unwrap_or(u32::MAX),
);
row.corpus_percentile_ci_low = Some(lo);
row.corpus_percentile_ci_high = Some(hi);
}
}
}
Ok(())
}
#[tracing::instrument(name = "code-health", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_code_health(db: &FactsDb, opts: &Options) -> Result<Vec<CodeHealthRow>> {
let cx = HealthScanCtx {
complexity_source: crate::analyses::grouped_complexity::source_table(opts).to_string(),
..HealthScanCtx::head()
};
run_code_health_scoped(db, opts, &cx)
}
pub fn run_code_health_scoped(
db: &FactsDb,
opts: &Options,
cx: &HealthScanCtx,
) -> Result<Vec<CodeHealthRow>> {
crate::analyses::lineage::materialize_source(db, opts)?;
let src_owned;
let src: &str = if let Some(ts) = &cx.history_cutoff {
let cutoff_sql = CHANGES_AT_TS_DDL.replace("{ts}", &ts.replace('\'', "''"));
db.conn()
.execute(&cutoff_sql, [])
.map_err(|e| CodeLoreError::Analysis(format!("create changes_at_ts view: {e}")))?;
src_owned = "changes_at_ts".to_string();
&src_owned
} else {
crate::analyses::lineage::source_table(opts)
};
materialize_centrality(db, opts, cx)?;
materialize_biomarkers(db, opts, cx)?;
let cm_src = &cx.complexity_source;
let tuned = crate::defect_calibration::active_weights(opts)?;
let weights = tuned
.as_ref()
.map_or_else(default_smell_weights, |(w, _)| w.clone());
let structural_scale_owned;
let structural_scale: &str = if cx.include_clones {
""
} else if let Some((w, _)) = &tuned {
let dry = w.iter().find(|(n, _)| n == "dry").map_or(0.0, |(_, v)| *v);
let divisor = 1.0 - dry;
if divisor > f64::EPSILON {
structural_scale_owned = format!(" / {divisor}");
&structural_scale_owned
} else {
""
}
} else {
STRUCTURAL_SCALE_NO_DRY
};
let sql = SQL
.replace("{smell_weights_case}", &smell_weights_case(&weights))
.replace("{src}", src)
.replace("{cm_src}", cm_src)
.replace("{structural_scale}", structural_scale)
.replace("{risk_red_min}", &crate::bands::RISK_RED_MIN.to_string())
.replace(
"{risk_yellow_min}",
&crate::bands::RISK_YELLOW_MIN.to_string(),
);
let row_limit: i64 = opts.rows_limit.map_or(i64::MAX, i64::from);
crate::analyses::query::explain_if_requested(
db,
&sql,
params![opts.min_revs, row_limit],
"code-health",
opts,
)?;
let mut stmt = db
.conn()
.prepare(&sql)
.map_err(|e| CodeLoreError::Analysis(format!("prepare code-health: {e}")))?;
let rows = stmt
.query_map(params![opts.min_revs, row_limit], |r| {
Ok(CodeHealthRow {
path: r.get::<_, String>(0)?,
cognitive: r.get::<_, f64>(1)?,
score: r.get::<_, f64>(2)?,
structural_risk: r.get::<_, f64>(3)?,
percentile: r.get::<_, f64>(4)?,
band: r.get::<_, String>(5)?,
corpus_percentile: None,
beyond_corpus: false,
corpus_percentile_ci_low: None,
corpus_percentile_ci_high: None,
})
})
.map_err(|e| CodeLoreError::Analysis(format!("query code-health: {e}")))?;
let mut rows = rows
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| CodeLoreError::Analysis(format!("collect code-health: {e}")))?;
apply_corpus_lens(db, opts, cx, &mut rows)?;
Ok(rows)
}
#[cfg(test)]
mod tests {
#[test]
fn head_ctx_defaults_to_head_tables() {
let c = super::HealthScanCtx::head();
assert_eq!(c.complexity_source, "complexity_metrics");
assert_eq!(c.imports_source, "imports");
assert!(c.history_cutoff.is_none());
assert!(c.include_clones);
}
#[test]
fn smell_weights_sum_to_one() {
let sum: f64 = super::SMELL_WEIGHTS.iter().map(|(_, w)| w).sum();
assert!(
(sum - 1.0).abs() < 1e-9,
"SMELL_WEIGHTS must sum to 1.0, got {sum}"
);
}
use crate::calibration::{
CALIBRATION_FORMAT_VERSION, CalibrationArtifact, LanguageTable, MetricQuantiles,
QUANTILE_POINTS, Stratum,
};
#[allow(clippy::cast_precision_loss)]
fn index_ramp() -> Vec<f64> {
(0..QUANTILE_POINTS).map(|i| i as f64).collect()
}
fn ramp_artifact(language: &str) -> CalibrationArtifact {
CalibrationArtifact {
format_version: CALIBRATION_FORMAT_VERSION,
corpus_vintage: "test-ramp".to_string(),
generated_at: "2026-07-12T00:00:00Z".to_string(),
repos_included: 1,
repos_attempted: 1,
languages: vec![LanguageTable {
language: language.to_string(),
sample_functions: 4_000,
strata: vec![Stratum {
sloc_min: 0,
sloc_max: u64::MAX,
metrics: vec![MetricQuantiles {
metric: "cyclomatic".to_string(),
quantiles: index_ramp(),
}],
}],
}],
repo_metrics: None,
}
}
#[test]
fn corpus_lens_resolves_q750_breakpoint() {
let art = ramp_artifact("rust");
let metrics = [("cyclomatic", Some(750.0)), ("cognitive", None)];
let (p, beyond) = super::file_corpus_lens(&art, "rust", &metrics)
.expect("covered metric must yield a percentile");
assert!((p - 0.75).abs() < 1e-9, "expected 0.75 at q750, got {p}");
assert!(!beyond, "a value inside the corpus is not beyond-corpus");
}
#[test]
fn corpus_lens_is_none_for_unknown_language() {
let art = ramp_artifact("rust");
let metrics = [("cyclomatic", Some(750.0))];
assert!(
super::file_corpus_lens(&art, "python", &metrics).is_none(),
"an uncovered language must yield no corpus lens"
);
}
#[test]
fn corpus_lens_saturates_and_flags_beyond_max() {
let art = ramp_artifact("rust");
let metrics = [("cyclomatic", Some(5_000.0))];
let (p, beyond) = super::file_corpus_lens(&art, "rust", &metrics)
.expect("a covered metric beyond the max still resolves");
assert!(
(p - 1.0).abs() < 1e-9,
"beyond-max must saturate to 1.0, got {p}"
);
assert!(
beyond,
"a value past the corpus maximum must flag beyond_corpus"
);
}
#[test]
fn corpus_lens_keeps_the_worst_dimension() {
let art = ramp_artifact("rust");
let metrics = [("cyclomatic", Some(250.0)), ("cyclomatic", Some(900.0))];
let (p, _) = super::file_corpus_lens(&art, "rust", &metrics).expect("covered");
assert!(
(p - 0.90).abs() < 1e-9,
"MAX must keep the worst (0.90), got {p}"
);
}
#[test]
fn corpus_lens_is_none_below_sample_floor() {
let mut art = ramp_artifact("rust");
art.languages[0].sample_functions = 10; let metrics = [("cyclomatic", Some(750.0))];
assert!(
super::file_corpus_lens(&art, "rust", &metrics).is_none(),
"an under-sampled language must yield no corpus lens"
);
}
#[test]
fn no_dry_scale_renormalizes_the_remaining_weights() {
let dry_weight = super::SMELL_WEIGHTS
.iter()
.find(|(s, _)| *s == "dry")
.map(|(_, w)| *w)
.expect("dry weight present");
let no_dry_sum: f64 = 1.0 - dry_weight;
assert_eq!(
super::STRUCTURAL_SCALE_NO_DRY,
format!(" / {no_dry_sum}"),
"no-DRY divisor must renormalize the remaining weights"
);
}
}