#[derive(Debug, Clone, Copy)]
pub struct ActivityThresholds {
pub days_since_last_commit_full_credit: f64,
pub days_since_last_commit_zero: f64,
pub commits_last_90d_full_credit: f64,
pub commits_last_90d_zero: f64,
pub active_contributors_full_credit: f64,
pub active_contributors_zero: f64,
pub median_issue_response_full_credit_hours: f64,
pub median_issue_response_zero_hours: f64,
pub days_since_last_release_full_credit: f64,
pub days_since_last_release_zero: f64,
pub min_repo_age_for_high_confidence_days: u64,
}
impl ActivityThresholds {
#[must_use]
pub const fn v1() -> Self {
Self {
days_since_last_commit_full_credit: 14.0,
days_since_last_commit_zero: 365.0,
commits_last_90d_full_credit: 30.0,
commits_last_90d_zero: 0.0,
active_contributors_full_credit: 4.0,
active_contributors_zero: 1.0,
median_issue_response_full_credit_hours: 48.0,
median_issue_response_zero_hours: 720.0,
days_since_last_release_full_credit: 90.0,
days_since_last_release_zero: 730.0,
min_repo_age_for_high_confidence_days: 180,
}
}
}
impl Default for ActivityThresholds {
fn default() -> Self {
Self::v1()
}
}
#[derive(Debug, Clone, Copy)]
pub struct MaintainerThresholds {
pub bus_factor_full_credit: u64,
pub gini_full_credit: f64,
pub gini_zero: f64,
pub retention_full_credit: f64,
pub retention_zero: f64,
pub min_repo_age_for_high_confidence_days: u64,
}
impl MaintainerThresholds {
#[must_use]
pub const fn v1() -> Self {
Self {
bus_factor_full_credit: 5,
gini_full_credit: 0.40,
gini_zero: 0.85,
retention_full_credit: 0.50,
retention_zero: 0.10,
min_repo_age_for_high_confidence_days: 180,
}
}
}
impl Default for MaintainerThresholds {
fn default() -> Self {
Self::v1()
}
}
#[derive(Debug, Clone, Copy)]
pub struct AdoptionThresholds {
pub downloads_band_25: u64,
pub downloads_band_50: u64,
pub downloads_band_75: u64,
pub downloads_band_100: u64,
pub readme_words_full_credit: u64,
pub readme_words_half_credit: u64,
pub high_confidence_downloads_floor: u64,
}
impl AdoptionThresholds {
#[must_use]
pub const fn v1() -> Self {
Self {
downloads_band_25: 1_000,
downloads_band_50: 10_000,
downloads_band_75: 100_000,
downloads_band_100: 1_000_000,
readme_words_full_credit: 500,
readme_words_half_credit: 100,
high_confidence_downloads_floor: 10_000,
}
}
}
impl Default for AdoptionThresholds {
fn default() -> Self {
Self::v1()
}
}
#[derive(Debug, Clone, Copy)]
pub struct StarsThresholds {
pub low_activity_bands: [(f64, u8); 6],
pub fork_to_star_healthy: f64,
pub watcher_to_star_healthy: f64,
pub min_sample_for_high_confidence: usize,
pub min_sample_for_medium_confidence: usize,
pub young_repo_age_days: u64,
pub young_repo_leniency_pp: f64,
pub min_stars_to_sample: u64,
pub lockstep_score_bands: [(f64, u8); 5],
pub combined_low_activity_threshold: f64,
pub combined_z_threshold: f64,
}
impl StarsThresholds {
#[must_use]
pub const fn v1() -> Self {
Self {
low_activity_bands: [
(0.05, 100),
(0.10, 85),
(0.20, 65),
(0.35, 40),
(0.50, 20),
(1.00, 0),
],
fork_to_star_healthy: 0.04,
watcher_to_star_healthy: 0.005,
min_sample_for_high_confidence: 100,
min_sample_for_medium_confidence: 30,
young_repo_age_days: 180,
young_repo_leniency_pp: 0.05,
min_stars_to_sample: 50,
lockstep_score_bands: [
(3.0, 100),
(5.0, 85),
(8.0, 60),
(12.0, 30),
(f64::INFINITY, 10),
],
combined_low_activity_threshold: 0.20,
combined_z_threshold: 5.0,
}
}
}
impl Default for StarsThresholds {
fn default() -> Self {
Self::v1()
}
}
#[must_use]
pub fn linear_lower_better(value: f64, full_credit: f64, zero: f64) -> u8 {
debug_assert!(
full_credit < zero,
"for lower-better signals, full_credit < zero"
);
if value <= full_credit {
100
} else if value >= zero {
0
} else {
let frac = (zero - value) / (zero - full_credit);
clamp_round(frac * 100.0)
}
}
#[must_use]
pub fn linear_higher_better(value: f64, full_credit: f64, zero: f64) -> u8 {
debug_assert!(
full_credit > zero,
"for higher-better signals, full_credit > zero"
);
if value >= full_credit {
100
} else if value <= zero {
0
} else {
let frac = (value - zero) / (full_credit - zero);
clamp_round(frac * 100.0)
}
}
fn clamp_round(x: f64) -> u8 {
x.round().clamp(0.0, 100.0) as u8
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lower_better_full_credit() {
assert_eq!(linear_lower_better(0.0, 14.0, 365.0), 100);
assert_eq!(linear_lower_better(14.0, 14.0, 365.0), 100);
}
#[test]
fn lower_better_zero() {
assert_eq!(linear_lower_better(365.0, 14.0, 365.0), 0);
assert_eq!(linear_lower_better(1000.0, 14.0, 365.0), 0);
}
#[test]
fn lower_better_midpoint() {
let s = linear_lower_better(189.5, 14.0, 365.0);
assert!((50..=51).contains(&s), "got {s}");
}
#[test]
fn higher_better_full_credit() {
assert_eq!(linear_higher_better(30.0, 30.0, 0.0), 100);
assert_eq!(linear_higher_better(100.0, 30.0, 0.0), 100);
}
#[test]
fn higher_better_zero() {
assert_eq!(linear_higher_better(0.0, 30.0, 0.0), 0);
}
#[test]
fn higher_better_midpoint() {
let s = linear_higher_better(15.0, 30.0, 0.0);
assert_eq!(s, 50);
}
#[test]
fn contributor_scoring_thresholds() {
assert_eq!(linear_higher_better(5.0, 4.0, 1.0), 100);
assert_eq!(linear_higher_better(4.0, 4.0, 1.0), 100);
assert_eq!(linear_higher_better(2.5, 4.0, 1.0), 50);
assert_eq!(linear_higher_better(1.0, 4.0, 1.0), 0);
}
#[test]
fn issue_response_lower_better() {
assert_eq!(linear_lower_better(48.0, 48.0, 720.0), 100);
assert_eq!(linear_lower_better(720.0, 48.0, 720.0), 0);
let s = linear_lower_better(384.0, 48.0, 720.0);
assert!((49..=51).contains(&s), "got {s}");
}
}