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#[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#[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, }
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#[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#[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#[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#[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#[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
155pub 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 fn applies_to(&self, _project_type: &ProjectType) -> bool {
164 true
165 }
166
167 fn check(&self, ctx: &ProjectContext) -> RuleResult;
169
170 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 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 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 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
219pub fn all_rules() -> Vec<Box<dyn Rule>> {
221 let mut rules: Vec<Box<dyn Rule>> = Vec::new();
222
223 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 rules.push(Box::new(drift::BuildCommandMatchesManifest));
235 rules.push(Box::new(drift::ReferencedFilesExist));
236
237 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 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 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 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 rules.extend(project_specific::project_specific_rules());
277
278 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}