use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct DiversityScore(f64);
impl DiversityScore {
pub fn new(value: f64) -> Result<Self> {
if !value.is_finite() {
anyhow::bail!(
"Diversity score must be finite, got {}",
if value.is_nan() { "NaN" } else { "infinity" }
);
}
if !(0.0..=1.0).contains(&value) {
anyhow::bail!("Diversity score must be between 0.0 and 1.0, got {}", value);
}
Ok(DiversityScore(value))
}
pub fn zero() -> Self {
DiversityScore(0.0)
}
pub fn as_f64(self) -> f64 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CrossDomainSeverity {
Critical,
High,
Medium,
Low,
}
impl CrossDomainSeverity {
pub fn as_str(self) -> &'static str {
match self {
CrossDomainSeverity::Critical => "CRITICAL",
CrossDomainSeverity::High => "HIGH",
CrossDomainSeverity::Medium => "MEDIUM",
CrossDomainSeverity::Low => "LOW",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DomainDiversityMetrics {
pub total_structs: usize,
pub domain_count: usize,
pub domain_distribution: HashMap<String, Vec<String>>,
pub diversity_score: DiversityScore,
pub severity: CrossDomainSeverity,
pub is_god_object: bool,
}
impl DomainDiversityMetrics {
pub fn from_struct_classifications(
classifications: &[StructDomainClassification],
is_god_object: bool,
) -> Result<Self> {
let total_structs = classifications.len();
if total_structs == 0 {
return Ok(DomainDiversityMetrics {
total_structs: 0,
domain_count: 0,
domain_distribution: HashMap::new(),
diversity_score: DiversityScore::zero(),
severity: CrossDomainSeverity::Low,
is_god_object,
});
}
let domain_distribution = group_structs_by_domain(classifications);
let domain_count = domain_distribution.len();
let diversity_score = calculate_domain_entropy(&domain_distribution, total_structs)
.context("Failed to calculate domain entropy")?;
let severity = determine_cross_domain_severity(total_structs, domain_count, is_god_object);
Ok(DomainDiversityMetrics {
total_structs,
domain_count,
domain_distribution,
diversity_score,
severity,
is_god_object,
})
}
pub fn largest_domain(&self) -> Option<(&String, usize)> {
self.domain_distribution
.iter()
.map(|(domain, structs)| (domain, structs.len()))
.max_by_key(|(_, count)| *count)
}
pub fn smallest_domain(&self) -> Option<(&String, usize)> {
self.domain_distribution
.iter()
.map(|(domain, structs)| (domain, structs.len()))
.min_by_key(|(_, count)| *count)
}
pub fn format_for_output(&self) -> String {
let mut output = String::new();
output.push_str(&format!(
"\nDOMAIN DIVERSITY ANALYSIS (Spec 140):\n\
Severity: {} - {} structs across {} domains\n\n",
self.severity.as_str(),
self.total_structs,
self.domain_count
));
output.push_str(&self.format_severity_justification());
output.push_str("\n\n");
output.push_str("Domain Distribution:\n");
let mut domains: Vec<_> = self.domain_distribution.iter().collect();
domains.sort_by_key(|(_, structs)| std::cmp::Reverse(structs.len()));
for (domain, structs) in domains {
output.push_str(&format!(
" - {}: {} structs ({:.0}%)\n",
domain,
structs.len(),
(structs.len() as f64 / self.total_structs as f64) * 100.0
));
let examples: Vec<&String> = structs.iter().take(3).collect();
output.push_str(&format!(
" Examples: {}{}",
examples
.iter()
.map(|s| s.as_str())
.collect::<Vec<&str>>()
.join(", "),
if structs.len() > 3 {
format!(", ... +{} more", structs.len() - 3)
} else {
String::new()
}
));
output.push('\n');
}
output.push_str(&format!(
"\nDiversity Score: {:.2} (0.0 = homogeneous, 1.0 = maximum diversity)\n",
self.diversity_score.as_f64()
));
if let Some((domain, count)) = self.largest_domain() {
output.push_str(&format!(
"Largest domain: {} ({} structs) - may need further splitting\n",
domain, count
));
}
if let Some((domain, count)) = self.smallest_domain() {
if count == 1 {
output.push_str(&format!(
"Singleton domain: {} (1 struct) - may merge or recategorize\n",
domain
));
}
}
output
}
fn format_severity_justification(&self) -> String {
match self.severity {
CrossDomainSeverity::Critical if self.is_god_object => {
format!(
" Reason: God object with {} domains detected (Spec 140: CRITICAL threshold)\n\
→ URGENT: Violates single responsibility principle at module level",
self.domain_count
)
}
CrossDomainSeverity::Critical => {
format!(
" Reason: {} structs across {} domains (Spec 140: CRITICAL threshold)\n\
→ URGENT: Massive cross-domain mixing",
self.total_structs, self.domain_count
)
}
CrossDomainSeverity::High => {
format!(
" Reason: {} structs across {} domains (Spec 140: HIGH threshold)\n\
→ HIGH PRIORITY: Significant organizational debt",
self.total_structs, self.domain_count
)
}
CrossDomainSeverity::Medium => {
format!(
" Reason: {} structs across {} domains (Spec 140: MEDIUM threshold)\n\
→ PROACTIVE: Good time to organize before it grows",
self.total_structs, self.domain_count
)
}
CrossDomainSeverity::Low => {
format!(
" Reason: {} structs across {} domains (Spec 140: LOW threshold)\n\
→ INFORMATIONAL: Minor organizational improvement opportunity",
self.total_structs, self.domain_count
)
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StructDomainClassification {
pub struct_name: String,
pub domain: String,
pub confidence: f64,
pub evidence: Vec<String>,
pub alternatives: Vec<(String, f64)>,
}
impl StructDomainClassification {
pub fn simple(struct_name: String, domain: String) -> Self {
StructDomainClassification {
struct_name,
domain,
confidence: 1.0,
evidence: vec![],
alternatives: vec![],
}
}
pub fn format_evidence(&self) -> String {
let mut output = format!(
"{} → {} domain ({:.0}% confidence)\n",
self.struct_name,
self.domain,
self.confidence * 100.0
);
if !self.evidence.is_empty() {
output.push_str(&format!(" Evidence: {}\n", self.evidence.join(", ")));
}
if !self.alternatives.is_empty() && self.alternatives[0].1 > 0.50 {
output.push_str(&format!(
" Alternative: {} ({:.0}%)\n",
self.alternatives[0].0,
self.alternatives[0].1 * 100.0
));
}
output
}
}
fn group_structs_by_domain(
classifications: &[StructDomainClassification],
) -> HashMap<String, Vec<String>> {
classifications
.iter()
.fold(HashMap::new(), |mut acc, classification| {
acc.entry(classification.domain.clone())
.or_default()
.push(classification.struct_name.clone());
acc
})
}
fn calculate_domain_entropy(
distribution: &HashMap<String, Vec<String>>,
total: usize,
) -> Result<DiversityScore> {
if total == 0 || distribution.is_empty() {
return Ok(DiversityScore::zero());
}
if distribution.len() == 1 {
return Ok(DiversityScore::zero());
}
let entropy: f64 = distribution
.values()
.map(|structs| {
let p = structs.len() as f64 / total as f64;
if p > 0.0 {
-p * p.log2()
} else {
0.0
}
})
.sum();
let max_entropy = (distribution.len() as f64).log2();
let normalized = if max_entropy > 0.0 {
(entropy / max_entropy).clamp(0.0, 1.0) } else {
0.0
};
DiversityScore::new(normalized).context("Entropy calculation produced invalid diversity score")
}
fn determine_cross_domain_severity(
struct_count: usize,
domain_count: usize,
is_god_object: bool,
) -> CrossDomainSeverity {
match (is_god_object, struct_count, domain_count) {
(true, _, d) if d >= 3 => CrossDomainSeverity::Critical,
(_, s, d) if s > 15 && d >= 5 => CrossDomainSeverity::Critical,
(_, s, d) if s >= 10 && d >= 4 => CrossDomainSeverity::High,
(_, s, d) if s >= 8 || d >= 3 => CrossDomainSeverity::Medium,
_ => CrossDomainSeverity::Low,
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
fn create_test_classifications(
struct_count: usize,
domain_count: usize,
) -> Vec<StructDomainClassification> {
let domains: Vec<String> = (0..domain_count).map(|i| format!("domain_{}", i)).collect();
(0..struct_count)
.map(|i| StructDomainClassification {
struct_name: format!("Struct{}", i),
domain: domains[i % domain_count].clone(),
confidence: 0.8,
evidence: vec![],
alternatives: vec![],
})
.collect()
}
fn create_uniform_domain_classifications(
struct_count: usize,
) -> Vec<StructDomainClassification> {
(0..struct_count)
.map(|i| StructDomainClassification {
struct_name: format!("Struct{}", i),
domain: "single_domain".to_string(),
confidence: 0.9,
evidence: vec![],
alternatives: vec![],
})
.collect()
}
#[test]
fn calculate_domain_diversity() {
let classifications = vec![
StructDomainClassification {
struct_name: "ScoringWeights".into(),
domain: "scoring".into(),
confidence: 0.90,
evidence: vec![],
alternatives: vec![],
},
StructDomainClassification {
struct_name: "ThresholdsConfig".into(),
domain: "thresholds".into(),
confidence: 0.85,
evidence: vec![],
alternatives: vec![],
},
StructDomainClassification {
struct_name: "RoleMultipliers".into(),
domain: "scoring".into(),
confidence: 0.80,
evidence: vec![],
alternatives: vec![],
},
];
let metrics = DomainDiversityMetrics::from_struct_classifications(&classifications, false)
.expect("Failed to create metrics");
assert_eq!(metrics.total_structs, 3);
assert_eq!(metrics.domain_count, 2); assert!(metrics.diversity_score.as_f64() > 0.0);
assert!(metrics.diversity_score.as_f64() < 1.0); }
#[test]
fn empty_classifications_handled_gracefully() {
let classifications: Vec<StructDomainClassification> = vec![];
let metrics = DomainDiversityMetrics::from_struct_classifications(&classifications, false)
.expect("Should handle empty classifications");
assert_eq!(metrics.total_structs, 0);
assert_eq!(metrics.domain_count, 0);
assert_eq!(metrics.diversity_score.as_f64(), 0.0);
assert_eq!(metrics.severity, CrossDomainSeverity::Low);
}
#[test]
fn single_domain_zero_diversity() {
let classifications = create_uniform_domain_classifications(10);
let metrics = DomainDiversityMetrics::from_struct_classifications(&classifications, false)
.expect("Failed to create metrics");
assert_eq!(metrics.domain_count, 1);
assert_eq!(metrics.diversity_score.as_f64(), 0.0); }
#[test]
fn diversity_score_type_safety() {
assert!(DiversityScore::new(0.0).is_ok());
assert!(DiversityScore::new(0.5).is_ok());
assert!(DiversityScore::new(1.0).is_ok());
assert!(DiversityScore::new(-0.1).is_err());
assert!(DiversityScore::new(1.1).is_err());
assert!(DiversityScore::new(f64::NAN).is_err());
}
#[test]
fn critical_severity_for_god_object_with_domains() {
let classifications = create_test_classifications(10, 3);
let metrics = DomainDiversityMetrics::from_struct_classifications(
&classifications,
true, )
.expect("Failed to create metrics");
assert_eq!(metrics.severity, CrossDomainSeverity::Critical);
}
#[test]
fn critical_severity_for_massive_mixing() {
let classifications = create_test_classifications(20, 5);
let metrics = DomainDiversityMetrics::from_struct_classifications(
&classifications,
false, )
.expect("Failed to create metrics");
assert_eq!(metrics.severity, CrossDomainSeverity::Critical);
}
#[test]
fn high_severity() {
let classifications = create_test_classifications(12, 4);
let metrics = DomainDiversityMetrics::from_struct_classifications(&classifications, false)
.expect("Failed to create metrics");
assert_eq!(metrics.severity, CrossDomainSeverity::High);
}
#[test]
fn medium_severity() {
let classifications = create_test_classifications(8, 3);
let metrics = DomainDiversityMetrics::from_struct_classifications(&classifications, false)
.expect("Failed to create metrics");
assert_eq!(metrics.severity, CrossDomainSeverity::Medium);
}
#[test]
fn low_severity() {
let classifications = create_test_classifications(4, 2);
let metrics = DomainDiversityMetrics::from_struct_classifications(&classifications, false)
.expect("Failed to create metrics");
assert_eq!(metrics.severity, CrossDomainSeverity::Low);
}
#[test]
fn largest_domain_detection() {
let classifications = vec![
StructDomainClassification::simple("A".into(), "domain1".into()),
StructDomainClassification::simple("B".into(), "domain1".into()),
StructDomainClassification::simple("C".into(), "domain1".into()),
StructDomainClassification::simple("D".into(), "domain2".into()),
];
let metrics = DomainDiversityMetrics::from_struct_classifications(&classifications, false)
.expect("Failed to create metrics");
let (domain, count) = metrics
.largest_domain()
.expect("Should have largest domain");
assert_eq!(domain.as_str(), "domain1");
assert_eq!(count, 3);
}
#[test]
fn smallest_domain_detection() {
let classifications = vec![
StructDomainClassification::simple("A".into(), "domain1".into()),
StructDomainClassification::simple("B".into(), "domain1".into()),
StructDomainClassification::simple("C".into(), "domain2".into()),
];
let metrics = DomainDiversityMetrics::from_struct_classifications(&classifications, false)
.expect("Failed to create metrics");
let (domain, count) = metrics
.smallest_domain()
.expect("Should have smallest domain");
assert_eq!(domain.as_str(), "domain2");
assert_eq!(count, 1);
}
#[test]
fn format_for_output_includes_spec_140_and_domain_distribution() {
let classifications = create_test_classifications(10, 3);
let metrics = DomainDiversityMetrics::from_struct_classifications(&classifications, true)
.expect("Failed to create metrics");
let output = metrics.format_for_output();
assert!(output.contains("DOMAIN DIVERSITY ANALYSIS"));
assert!(output.contains("Spec 140"));
assert!(output.contains("Severity:"));
assert!(output.contains("domains"));
assert!(output.contains("Domain Distribution:"));
assert!(output.contains("Diversity Score:"));
}
#[test]
fn format_for_output_uses_domain_count_not_responsibilities() {
let classifications = create_test_classifications(10, 3);
let metrics = DomainDiversityMetrics::from_struct_classifications(&classifications, true)
.expect("Failed to create metrics");
let output = metrics.format_for_output();
assert!(output.contains("10 structs across 3 domains"));
assert!(!output.contains("across 1 responsibilities"));
}
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn diversity_score_always_bounded(
struct_count in 1usize..100,
domain_count in 1usize..20
) {
let classifications = create_test_classifications(struct_count, domain_count.min(struct_count));
let metrics = DomainDiversityMetrics::from_struct_classifications(&classifications, false)
.expect("Failed to create metrics");
prop_assert!(metrics.diversity_score.as_f64() >= 0.0);
prop_assert!(metrics.diversity_score.as_f64() <= 1.0);
}
#[test]
fn single_domain_always_zero_diversity(struct_count in 1usize..100) {
let classifications = create_uniform_domain_classifications(struct_count);
let metrics = DomainDiversityMetrics::from_struct_classifications(&classifications, false)
.expect("Failed to create metrics");
prop_assert_eq!(metrics.diversity_score.as_f64(), 0.0);
prop_assert_eq!(metrics.domain_count, 1);
}
#[test]
fn struct_count_matches_input(
struct_count in 0usize..100,
domain_count in 1usize..20
) {
let classifications = if struct_count == 0 {
vec![]
} else {
create_test_classifications(struct_count, domain_count.min(struct_count))
};
let metrics = DomainDiversityMetrics::from_struct_classifications(&classifications, false)
.expect("Failed to create metrics");
prop_assert_eq!(metrics.total_structs, struct_count);
}
#[test]
fn god_object_with_multiple_domains_is_critical(
struct_count in 5usize..100,
domain_count in 3usize..20
) {
let classifications = create_test_classifications(struct_count, domain_count.min(struct_count));
let metrics = DomainDiversityMetrics::from_struct_classifications(&classifications, true)
.expect("Failed to create metrics");
prop_assert_eq!(metrics.severity, CrossDomainSeverity::Critical);
}
#[test]
fn domain_count_never_exceeds_struct_count(
struct_count in 1usize..100,
domain_count in 1usize..20
) {
let classifications = create_test_classifications(struct_count, domain_count);
let metrics = DomainDiversityMetrics::from_struct_classifications(&classifications, false)
.expect("Failed to create metrics");
prop_assert!(metrics.domain_count <= metrics.total_structs);
}
}
}
}