1use serde::{Deserialize, Serialize};
2
3use crate::finding::{Category, Finding, Severity};
4
5#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
7pub struct CategoryScores {
8 pub seo: f64,
9 pub accessibility: f64,
10 pub structured_data: f64,
11 pub llm_readiness: f64,
12 pub overall: f64,
13}
14
15impl CategoryScores {
16 pub fn from_findings(findings: &[Finding]) -> Self {
17 let seo = score_category(findings, Category::Seo);
18 let accessibility = score_category(findings, Category::Accessibility);
19 let structured_data = score_category(findings, Category::StructuredData);
20 let llm_readiness = score_category(findings, Category::LlmReadiness);
21
22 let overall =
24 seo * 0.30 + accessibility * 0.25 + structured_data * 0.25 + llm_readiness * 0.20;
25
26 Self {
27 seo,
28 accessibility,
29 structured_data,
30 llm_readiness,
31 overall,
32 }
33 }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ScoreBreakdown {
38 pub scores: CategoryScores,
39 pub penalty_weights: PenaltyWeights,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct PenaltyWeights {
44 pub error: u32,
45 pub warning: u32,
46 pub info: u32,
47}
48
49impl Default for PenaltyWeights {
50 fn default() -> Self {
51 Self {
52 error: 10,
53 warning: 5,
54 info: 1,
55 }
56 }
57}
58
59fn score_category(findings: &[Finding], category: Category) -> f64 {
60 let weights = PenaltyWeights::default();
61 let penalty: u32 = findings
62 .iter()
63 .filter(|f| f.category == category)
64 .map(|f| match f.severity {
65 Severity::Error => weights.error,
66 Severity::Warning => weights.warning,
67 Severity::Info => weights.info,
68 })
69 .sum();
70
71 (100.0_f64 - f64::from(penalty)).clamp(0.0, 100.0)
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use crate::finding::Finding;
78
79 #[test]
80 fn perfect_score_with_no_findings() {
81 let scores = CategoryScores::from_findings(&[]);
82 assert!((scores.overall - 100.0).abs() < f64::EPSILON);
83 }
84
85 #[test]
86 fn errors_reduce_score() {
87 let findings = vec![Finding::new(
88 "test",
89 Severity::Error,
90 Category::Seo,
91 "test error",
92 )];
93 let scores = CategoryScores::from_findings(&findings);
94 assert!((scores.seo - 90.0).abs() < f64::EPSILON);
95 }
96
97 #[test]
98 fn performance_hint_does_not_affect_scores() {
99 let findings = vec![Finding::new(
100 "perf-hint",
101 Severity::Info,
102 Category::PerformanceHint,
103 "perf hint",
104 )];
105 let scores = CategoryScores::from_findings(&findings);
106 assert!((scores.overall - 100.0).abs() < f64::EPSILON);
107 }
108}