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#[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#[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, }
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#[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#[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#[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#[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#[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
150pub 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 fn applies_to(&self, _project_type: &ProjectType) -> bool {
159 true
160 }
161
162 fn check(&self, ctx: &ProjectContext) -> RuleResult;
164
165 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 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 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 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
214pub fn all_rules() -> Vec<Box<dyn Rule>> {
216 let mut rules: Vec<Box<dyn Rule>> = Vec::new();
217
218 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 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 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 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 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 rules.extend(project_specific::project_specific_rules());
265
266 rules
267}