neti 1.6.6

High-integrity code quality governance and transactional change management for AI-assisted development
Documentation
use std::collections::HashMap;
use std::path::PathBuf;

use crate::graph::locality::types::{Coupling, LocalityEdge};
use crate::graph::locality::ValidationReport;

use super::violations::CategorizedViolation;

#[derive(Debug, Clone)]
pub struct GodModuleInfo {
    pub path: PathBuf,
    pub outbound_violations: usize,
    pub targets: Vec<PathBuf>,
}

#[derive(Debug, Clone)]
pub struct HubCandidate {
    pub path: PathBuf,
    pub fan_in: usize,
    pub importers: Vec<PathBuf>,
}

#[must_use]
pub fn find_god_modules(violations: &[CategorizedViolation]) -> Vec<GodModuleInfo> {
    let mut by_source: HashMap<PathBuf, Vec<PathBuf>> = HashMap::new();

    for v in violations {
        by_source
            .entry(v.edge.from.clone())
            .or_default()
            .push(v.edge.to.clone());
    }

    by_source
        .into_iter()
        .filter(|(_, targets)| targets.len() >= 3)
        .map(|(path, targets)| GodModuleInfo {
            path,
            outbound_violations: targets.len(),
            targets,
        })
        .collect()
}

#[must_use]
#[allow(clippy::implicit_hasher)]
pub fn find_hub_candidates(
    couplings: &HashMap<PathBuf, Coupling>,
    report: &ValidationReport,
) -> Vec<HubCandidate> {
    let mut target_importers: HashMap<PathBuf, Vec<PathBuf>> = HashMap::new();

    for edge in report.failed() {
        target_importers
            .entry(edge.to.clone())
            .or_default()
            .push(edge.from.clone());
    }

    target_importers
        .into_iter()
        .filter(|(_, importers)| importers.len() >= 2)
        .map(|(path, importers)| {
            let fan_in = couplings.get(&path).map_or(0, Coupling::afferent);
            HubCandidate { path, fan_in, importers }
        })
        .collect()
}

#[must_use]
pub fn compute_module_coupling(edges: &[LocalityEdge]) -> Vec<(String, String, usize)> {
    let mut coupling: HashMap<(String, String), usize> = HashMap::new();

    for edge in edges {
        let from_mod = get_top_module(&edge.from);
        let to_mod = get_top_module(&edge.to);
        if from_mod != to_mod {
            let key = order_pair(from_mod, to_mod);
            *coupling.entry(key).or_insert(0) += 1;
        }
    }

    let mut result: Vec<_> = coupling.into_iter().map(|((a, b), c)| (a, b, c)).collect();
    result.sort_by(|a, b| b.2.cmp(&a.2));
    result
}

fn get_top_module(path: &std::path::Path) -> String {
    let parts: Vec<_> = path.components().collect();
    let src_idx = parts.iter().position(|c| c.as_os_str() == "src");
    src_idx
        .and_then(|i| parts.get(i + 1))
        .and_then(|c| c.as_os_str().to_str())
        .unwrap_or("unknown")
        .to_string()
}

fn order_pair(a: String, b: String) -> (String, String) {
    if a < b { (a, b) } else { (b, a) }
}