1pub mod weights;
2
3use std::collections::HashMap;
4
5use crate::detection::ProjectType;
6use crate::rules::{Dimension, Effort, RuleResult, RuleStatus, Severity, Suggestion, SuggestionPriority};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub enum Grade {
11 F,
12 D,
13 C,
14 B,
15 A,
16 APlus,
17}
18
19impl Grade {
20 pub fn from_score(score: f64) -> Self {
21 match score as u32 {
22 90..=100 => Grade::APlus,
23 80..=89 => Grade::A,
24 70..=79 => Grade::B,
25 60..=69 => Grade::C,
26 40..=59 => Grade::D,
27 _ => Grade::F,
28 }
29 }
30}
31
32impl std::fmt::Display for Grade {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 Grade::APlus => write!(f, "A+"),
36 Grade::A => write!(f, "A"),
37 Grade::B => write!(f, "B"),
38 Grade::C => write!(f, "C"),
39 Grade::D => write!(f, "D"),
40 Grade::F => write!(f, "F"),
41 }
42 }
43}
44
45impl Grade {
46 pub fn description(&self) -> &str {
47 match self {
48 Grade::APlus => "Fully Claude Native — optimized for AI-assisted development",
49 Grade::A => "Claude Native — well set up with minor improvements possible",
50 Grade::B => "Claude Friendly — good foundation, notable gaps",
51 Grade::C => "Claude Compatible — works but significant optimization possible",
52 Grade::D => "Claude Hostile — major friction, high token waste",
53 Grade::F => "Not Claude Native — needs fundamental restructuring",
54 }
55 }
56}
57
58#[derive(Debug, Clone)]
60pub struct DimensionScore {
61 pub dimension: Dimension,
62 pub score: f64,
63 pub weight: f64,
64 pub rules_passed: usize,
65 pub rules_failed: usize,
66 pub rules_warned: usize,
67 pub rules_skipped: usize,
68 pub capped: bool,
69}
70
71#[derive(Debug, Clone)]
73pub struct Scorecard {
74 pub project_type: ProjectType,
75 pub dimensions: Vec<DimensionScore>,
76 pub total_score: f64,
77 pub grade: Grade,
78 pub rule_results: Vec<RuleResult>,
79 pub suggestions: Vec<Suggestion>,
80}
81
82const ALL_DIMENSIONS: [Dimension; 5] = [
83 Dimension::Foundation,
84 Dimension::ContextEfficiency,
85 Dimension::Navigation,
86 Dimension::Tooling,
87 Dimension::CodeQuality,
88];
89
90pub fn calculate(results: Vec<RuleResult>, project_type: &ProjectType) -> Scorecard {
92 let weight_table = weights::get_weights(project_type);
93 let mut by_dimension: HashMap<Dimension, Vec<&RuleResult>> = HashMap::new();
94 for r in &results {
95 by_dimension.entry(r.dimension).or_default().push(r);
96 }
97
98 let dim_scores: Vec<DimensionScore> = ALL_DIMENSIONS.iter()
99 .map(|dim| score_dimension(*dim, &by_dimension, &weight_table))
100 .collect();
101
102 let total_score = dim_scores.iter().map(|d| d.score * d.weight).sum::<f64>().clamp(0.0, 100.0);
103 let grade = Grade::from_score(total_score);
104 let suggestions = collect_suggestions(&results, total_score);
105
106 Scorecard { project_type: project_type.clone(), dimensions: dim_scores, total_score, grade, rule_results: results, suggestions }
107}
108
109fn score_dimension(
110 dim: Dimension,
111 by_dimension: &HashMap<Dimension, Vec<&RuleResult>>,
112 weight_table: &HashMap<Dimension, f64>,
113) -> DimensionScore {
114 let dim_results = by_dimension.get(&dim).cloned().unwrap_or_default();
115 let weight = weight_table.get(&dim).copied().unwrap_or(0.2);
116 let mut score = 100.0_f64;
117 let mut capped = false;
118 let (mut passed, mut failed, mut warned, mut skipped) = (0, 0, 0, 0);
119
120 for r in &dim_results {
121 match &r.status {
122 RuleStatus::Pass => passed += 1,
123 RuleStatus::Skip => skipped += 1,
124 RuleStatus::Warn(_) => { warned += 1; score -= r.severity.deduction() / 2.0; }
125 RuleStatus::Fail(_) => {
126 failed += 1;
127 if r.severity == Severity::Critical { capped = true; }
128 else { score -= r.severity.deduction(); }
129 }
130 }
131 }
132
133 if capped { score = score.min(30.0); }
134 score = score.clamp(0.0, 100.0);
135
136 DimensionScore { dimension: dim, score, weight, rules_passed: passed, rules_failed: failed, rules_warned: warned, rules_skipped: skipped, capped }
137}
138
139fn collect_suggestions(results: &[RuleResult], total_score: f64) -> Vec<Suggestion> {
140 let mut suggestions: Vec<Suggestion> = results.iter()
141 .filter_map(|r| r.suggestion.clone())
142 .collect();
143 suggestions.sort_by_key(|s| s.priority);
144
145 if total_score < 60.0 {
147 suggestions.insert(0, Suggestion {
148 priority: SuggestionPriority::QuickWin,
149 title: "Run `claude-native --init` to bootstrap your project".into(),
150 description: "This single command generates CLAUDE.md, .claudeignore, and .claude/settings.json tailored to your detected project type. Typically jumps score by 20-30 points.".into(),
151 effort: Effort::Minutes,
152 });
153 }
154
155 suggestions
156}