Skip to main content

claude_native/rules/
mod.rs

1pub mod foundation;
2pub mod context;
3pub mod context_extra;
4pub mod navigation;
5pub mod navigation_extra;
6pub mod tooling;
7pub mod quality;
8pub mod quality_extra;
9pub mod project_specific;
10
11use crate::detection::ProjectType;
12use crate::scan::ProjectContext;
13
14/// Scoring dimensions
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum Dimension {
17    Foundation,
18    ContextEfficiency,
19    Navigation,
20    Tooling,
21    CodeQuality,
22}
23
24impl std::fmt::Display for Dimension {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            Dimension::Foundation => write!(f, "Foundation"),
28            Dimension::ContextEfficiency => write!(f, "Context Efficiency"),
29            Dimension::Navigation => write!(f, "Navigation"),
30            Dimension::Tooling => write!(f, "Tooling"),
31            Dimension::CodeQuality => write!(f, "Code Quality"),
32        }
33    }
34}
35
36/// Rule severity
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
38pub enum Severity {
39    Low,
40    Medium,
41    High,
42    Critical,
43}
44
45impl Severity {
46    pub fn deduction(&self) -> f64 {
47        match self {
48            Severity::Low => 5.0,
49            Severity::Medium => 10.0,
50            Severity::High => 20.0,
51            Severity::Critical => 0.0, // caps at 30 instead
52        }
53    }
54}
55
56impl std::fmt::Display for Severity {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match self {
59            Severity::Low => write!(f, "LOW"),
60            Severity::Medium => write!(f, "MEDIUM"),
61            Severity::High => write!(f, "HIGH"),
62            Severity::Critical => write!(f, "CRITICAL"),
63        }
64    }
65}
66
67/// Outcome of a single rule check
68#[derive(Debug, Clone)]
69pub enum RuleStatus {
70    Pass,
71    Warn(String),
72    Fail(String),
73    Skip,
74}
75
76impl RuleStatus {
77    pub fn is_failure(&self) -> bool {
78        matches!(self, RuleStatus::Fail(_))
79    }
80
81    pub fn is_warning(&self) -> bool {
82        matches!(self, RuleStatus::Warn(_))
83    }
84
85    pub fn is_pass(&self) -> bool {
86        matches!(self, RuleStatus::Pass)
87    }
88
89    pub fn is_skip(&self) -> bool {
90        matches!(self, RuleStatus::Skip)
91    }
92}
93
94/// Suggestion priority
95#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
96pub enum SuggestionPriority {
97    QuickWin,
98    HighImpact,
99    NiceToHave,
100}
101
102impl std::fmt::Display for SuggestionPriority {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        match self {
105            SuggestionPriority::QuickWin => write!(f, "Quick Win"),
106            SuggestionPriority::HighImpact => write!(f, "High Impact"),
107            SuggestionPriority::NiceToHave => write!(f, "Nice to Have"),
108        }
109    }
110}
111
112/// Effort estimate
113#[derive(Debug, Clone, Copy)]
114pub enum Effort {
115    Minutes,
116    Hour,
117    HalfDay,
118}
119
120impl std::fmt::Display for Effort {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        match self {
123            Effort::Minutes => write!(f, "~2 min"),
124            Effort::Hour => write!(f, "~1 hour"),
125            Effort::HalfDay => write!(f, "~half day"),
126        }
127    }
128}
129
130/// A suggestion attached to a failed/warned rule
131#[derive(Debug, Clone)]
132pub struct Suggestion {
133    pub priority: SuggestionPriority,
134    pub title: String,
135    pub description: String,
136    pub effort: Effort,
137}
138
139/// Complete result of evaluating one rule
140#[derive(Debug, Clone)]
141pub struct RuleResult {
142    pub rule_id: String,
143    pub name: String,
144    pub dimension: Dimension,
145    pub severity: Severity,
146    pub status: RuleStatus,
147    pub suggestion: Option<Suggestion>,
148}
149
150/// The rule trait — each check implements this
151pub trait Rule: Send + Sync {
152    fn id(&self) -> &str;
153    fn name(&self) -> &str;
154    fn dimension(&self) -> Dimension;
155    fn severity(&self) -> Severity;
156
157    /// Whether this rule applies to the detected project type.
158    fn applies_to(&self, _project_type: &ProjectType) -> bool {
159        true
160    }
161
162    /// Execute the check.
163    fn check(&self, ctx: &ProjectContext) -> RuleResult;
164
165    /// Helper to produce a pass result.
166    fn pass(&self) -> RuleResult {
167        RuleResult {
168            rule_id: self.id().to_string(),
169            name: self.name().to_string(),
170            dimension: self.dimension(),
171            severity: self.severity(),
172            status: RuleStatus::Pass,
173            suggestion: None,
174        }
175    }
176
177    /// Helper to produce a fail result with suggestion.
178    fn fail(&self, reason: &str, suggestion: Suggestion) -> RuleResult {
179        RuleResult {
180            rule_id: self.id().to_string(),
181            name: self.name().to_string(),
182            dimension: self.dimension(),
183            severity: self.severity(),
184            status: RuleStatus::Fail(reason.to_string()),
185            suggestion: Some(suggestion),
186        }
187    }
188
189    /// Helper to produce a warn result with suggestion.
190    fn warn(&self, reason: &str, suggestion: Suggestion) -> RuleResult {
191        RuleResult {
192            rule_id: self.id().to_string(),
193            name: self.name().to_string(),
194            dimension: self.dimension(),
195            severity: self.severity(),
196            status: RuleStatus::Warn(reason.to_string()),
197            suggestion: Some(suggestion),
198        }
199    }
200
201    /// Helper to produce a skip result.
202    fn skip(&self) -> RuleResult {
203        RuleResult {
204            rule_id: self.id().to_string(),
205            name: self.name().to_string(),
206            dimension: self.dimension(),
207            severity: self.severity(),
208            status: RuleStatus::Skip,
209            suggestion: None,
210        }
211    }
212}
213
214/// Collect all rules (base + project-specific)
215pub fn all_rules() -> Vec<Box<dyn Rule>> {
216    let mut rules: Vec<Box<dyn Rule>> = Vec::new();
217
218    // Foundation (1.1 - 1.7)
219    rules.push(Box::new(foundation::ClaudeMdExists));
220    rules.push(Box::new(foundation::ClaudeMdConcise));
221    rules.push(Box::new(foundation::ClaudeMdActionable));
222    rules.push(Box::new(foundation::ClaudeMdHasCommands));
223    rules.push(Box::new(foundation::ClaudeignoreExists));
224    rules.push(Box::new(foundation::ClaudeDirExists));
225    rules.push(Box::new(foundation::SettingsJsonExists));
226
227    // Context Efficiency (2.1 - 2.7)
228    rules.push(Box::new(context::NoMegaFiles));
229    rules.push(Box::new(context::NoMegaFunctions));
230    rules.push(Box::new(context::LockFilesIgnored));
231    rules.push(Box::new(context::GeneratedFilesIgnored));
232    rules.push(Box::new(context_extra::NoSecretsInRepo));
233    rules.push(Box::new(context_extra::ReadmeExistsAndConcise));
234    rules.push(Box::new(context_extra::SubdirClaudeMd));
235
236    // Navigation (3.1 - 3.7)
237    rules.push(Box::new(navigation::ClearDirectoryStructure));
238    rules.push(Box::new(navigation::ConsistentNaming));
239    rules.push(Box::new(navigation::ObviousEntryPoints));
240    rules.push(Box::new(navigation_extra::ClearModuleBoundaries));
241    rules.push(Box::new(navigation_extra::PredictableTestLocations));
242    rules.push(Box::new(navigation_extra::NoDeepNesting));
243    rules.push(Box::new(navigation_extra::DescriptiveNames));
244
245    // Tooling (4.1 - 4.6)
246    rules.push(Box::new(tooling::McpServersConfigured));
247    rules.push(Box::new(tooling::AutoFormatHook));
248    rules.push(Box::new(tooling::DangerousOpProtection));
249    rules.push(Box::new(tooling::CustomSkills));
250    rules.push(Box::new(tooling::PermissionAllowList));
251    rules.push(Box::new(tooling::PathScopedRules));
252
253    // Code Quality (5.1 - 5.8)
254    rules.push(Box::new(quality::TypeAnnotationsExist));
255    rules.push(Box::new(quality::TestsExist));
256    rules.push(Box::new(quality_extra::DescriptiveTestNames));
257    rules.push(Box::new(quality_extra::ConsistentPatterns));
258    rules.push(Box::new(quality_extra::CommentsExplainWhy));
259    rules.push(Box::new(quality_extra::NoDeadCode));
260    rules.push(Box::new(quality_extra::DependenciesDocumented));
261    rules.push(Box::new(quality_extra::CiCdExists));
262
263    // Project-specific rules (filtered by applies_to at runtime)
264    rules.extend(project_specific::project_specific_rules());
265
266    rules
267}