Skip to main content

claude_native/rules/
mod.rs

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