repopilot 0.11.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use crate::baseline::diff::BaselineStatus;
use serde::{Deserialize, Serialize};

pub const FORMULA_VERSION: &str = "risk-v2";

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RiskAssessment {
    pub score: u8,
    pub priority: RiskPriority,
    pub signals: Vec<RiskSignal>,
    pub formula_version: String,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum RiskPriority {
    P0,
    P1,
    P2,
    P3,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RiskSignal {
    pub id: String,
    pub label: String,
    pub weight: i16,
    pub reason: String,
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct RiskInputs {
    pub baseline_status: Option<BaselineStatus>,
    pub in_diff: bool,
    pub workspace_hotspot: bool,
    pub graph_impact: Option<GraphImpact>,
    pub blast_radius: bool,
    pub cluster_size: usize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GraphImpact {
    Hub,
    Dependency,
}

impl Default for RiskAssessment {
    fn default() -> Self {
        Self {
            score: 0,
            priority: RiskPriority::P3,
            signals: Vec::new(),
            formula_version: FORMULA_VERSION.to_string(),
        }
    }
}

impl RiskAssessment {
    pub(super) fn new(score: u8, signals: Vec<RiskSignal>) -> Self {
        Self {
            score,
            priority: priority_for_score(score),
            signals,
            formula_version: FORMULA_VERSION.to_string(),
        }
    }
}

pub fn priority_for_score(score: u8) -> RiskPriority {
    match score {
        90..=100 => RiskPriority::P0,
        70..=89 => RiskPriority::P1,
        40..=69 => RiskPriority::P2,
        _ => RiskPriority::P3,
    }
}

impl RiskPriority {
    pub fn label(self) -> &'static str {
        match self {
            Self::P0 => "P0",
            Self::P1 => "P1",
            Self::P2 => "P2",
            Self::P3 => "P3",
        }
    }
}

pub(super) fn push_adjustment(
    score: &mut i16,
    signals: &mut Vec<RiskSignal>,
    id: &str,
    label: &str,
    weight: i16,
    reason: &str,
) {
    push_signal(score, signals, signal(id, label, weight, reason));
}

pub(super) fn push_signal(score: &mut i16, signals: &mut Vec<RiskSignal>, signal: RiskSignal) {
    *score += signal.weight;
    signals.push(signal);
}

pub(super) fn signal(id: &str, label: &str, weight: i16, reason: &str) -> RiskSignal {
    RiskSignal {
        id: id.to_string(),
        label: label.to_string(),
        weight,
        reason: reason.to_string(),
    }
}

pub(super) fn clamp_score(score: i16) -> u8 {
    score.clamp(0, 100) as u8
}