#![cfg_attr(coverage_nightly, coverage(off))]
use crate::services::normalized_score::NormalizedScore;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;
pub const REPO_SCORE_MAX_POINTS: f64 = 110.0;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoScore {
pub total_score: f64,
pub grade: Grade, pub categories: CategoryScores,
pub recommendations: Vec<Recommendation>,
pub metadata: ScoreMetadata,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum Grade {
APlus, A, AMinus, BPlus, B, C, D, F, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CategoryScores {
pub documentation: CategoryScore, pub precommit_hooks: CategoryScore, pub repository_hygiene: CategoryScore, pub build_test_automation: CategoryScore, pub continuous_integration: CategoryScore, pub pmat_compliance: CategoryScore, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CategoryScore {
pub score: f64, pub max_score: f64, pub percentage: f64, pub status: ScoreStatus, pub subcategories: Vec<SubcategoryScore>,
pub findings: Vec<Finding>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubcategoryScore {
pub id: String, pub name: String, pub score: f64,
pub max_score: f64,
pub findings: Vec<Finding>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BonusScores {
pub property_tests: BonusItem, pub fuzzing: BonusItem, pub mutation_testing: BonusItem, pub living_docs: BonusItem, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BonusItem {
pub points: f64,
pub max_points: f64,
pub detected: bool,
pub evidence: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Finding {
pub severity: Severity,
pub category: String,
pub message: String,
pub location: Option<String>, pub impact_points: f64, }
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum Severity {
Success, Warning, Error, Info, }
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ScoreStatus {
Pass, Warning, Fail, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Recommendation {
pub priority: Priority,
pub category: String,
pub title: String,
pub description: String,
pub impact_points: f64, pub estimated_effort: String, pub commands: Vec<String>, }
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum Priority {
Critical, High, Medium, Low, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScoreMetadata {
pub timestamp: String, pub repository_path: PathBuf,
pub git_branch: Option<String>,
pub git_commit: Option<String>,
pub pmat_version: String,
pub spec_version: String, pub execution_time_ms: u64,
}
include!("models_impls.rs");
include!("models_tests.rs");
#[cfg(kani)]
mod kani_proofs {
use super::{CategoryScore, Grade, ScoreStatus};
#[kani::proof]
fn grade_from_score_total() {
let s: f64 = kani::any();
kani::assume(s.is_finite());
kani::assume((-1000.0..=1000.0).contains(&s));
let g = Grade::from_score(s);
let _ok = matches!(
g,
Grade::APlus
| Grade::A
| Grade::AMinus
| Grade::BPlus
| Grade::B
| Grade::C
| Grade::D
| Grade::F
);
assert!(_ok);
}
#[kani::proof]
fn score_ge_95_is_a_plus() {
let s: f64 = kani::any();
kani::assume(s.is_finite());
kani::assume((95.0..=1000.0).contains(&s));
assert!(matches!(Grade::from_score(s), Grade::APlus));
}
#[kani::proof]
fn category_score_percentage_invariant() {
let score: f64 = kani::any();
let max_score: f64 = kani::any();
kani::assume(score.is_finite() && max_score.is_finite());
kani::assume((0.0..=1000.0).contains(&score));
kani::assume((1.0..=1000.0).contains(&max_score));
let cs = CategoryScore::new(score, max_score, Vec::new(), Vec::new());
let expected = (score / max_score) * 100.0;
assert!(cs.percentage == expected);
let status_matches = match cs.status {
ScoreStatus::Pass => expected >= 90.0,
ScoreStatus::Warning => expected >= 70.0 && expected < 90.0,
ScoreStatus::Fail => expected < 70.0,
};
assert!(status_matches);
}
#[kani::proof]
fn category_score_zero_max_is_safe() {
let score: f64 = kani::any();
kani::assume(score.is_finite());
kani::assume((0.0..=1000.0).contains(&score));
let cs = CategoryScore::new(score, 0.0, Vec::new(), Vec::new());
assert!(cs.percentage == 0.0);
assert!(matches!(cs.status, ScoreStatus::Fail));
}
}