1use std::collections::HashMap;
4use std::path::PathBuf;
5
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
11pub struct CommitContext {
12 pub project: ProjectContext,
14 pub branch: BranchContext,
16 pub range: CommitRangeContext,
18 pub files: Vec<FileContext>,
20 pub user_provided: Option<String>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, Default)]
26pub struct ProjectContext {
27 pub commit_guidelines: Option<String>,
29 pub pr_guidelines: Option<String>,
31 pub valid_scopes: Vec<ScopeDefinition>,
33 pub feature_contexts: HashMap<String, FeatureContext>,
35 pub project_conventions: ProjectConventions,
37 pub ecosystem: Ecosystem,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ScopeDefinition {
44 pub name: String,
46 pub description: String,
48 pub examples: Vec<String>,
50 pub file_patterns: Vec<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct FeatureContext {
57 pub name: String,
59 pub description: String,
61 pub scope: String,
63 pub conventions: Vec<String>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, Default)]
69pub struct ProjectConventions {
70 pub commit_format: Option<String>,
72 pub required_trailers: Vec<String>,
74 pub preferred_types: Vec<String>,
76 pub scope_requirements: ScopeRequirements,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, Default)]
82pub struct ScopeRequirements {
83 pub required: bool,
85 pub valid_scopes: Vec<String>,
87 pub scope_mapping: HashMap<String, Vec<String>>, }
90
91#[derive(Debug, Clone, Serialize, Deserialize, Default)]
93pub enum Ecosystem {
94 #[default]
95 Unknown,
97 Rust,
99 Node,
101 Python,
103 Go,
105 Java,
107 Generic,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, Default)]
113pub struct BranchContext {
114 pub work_type: WorkType,
116 pub scope: Option<String>,
118 pub ticket_id: Option<String>,
120 pub description: String,
122 pub is_feature_branch: bool,
124 pub base_branch: Option<String>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, Default)]
130pub enum WorkType {
131 #[default]
132 Unknown,
134 Feature,
136 Fix,
138 Docs,
140 Refactor,
142 Chore,
144 Test,
146 Ci,
148 Build,
150 Perf,
152}
153
154impl std::str::FromStr for WorkType {
155 type Err = anyhow::Error;
156
157 fn from_str(s: &str) -> Result<Self> {
158 match s.to_lowercase().as_str() {
159 "feature" | "feat" => Ok(Self::Feature),
160 "fix" | "bugfix" => Ok(Self::Fix),
161 "docs" | "doc" => Ok(Self::Docs),
162 "refactor" | "refact" => Ok(Self::Refactor),
163 "chore" => Ok(Self::Chore),
164 "test" | "tests" => Ok(Self::Test),
165 "ci" => Ok(Self::Ci),
166 "build" => Ok(Self::Build),
167 "perf" | "performance" => Ok(Self::Perf),
168 _ => Ok(Self::Unknown),
169 }
170 }
171}
172
173impl std::fmt::Display for WorkType {
174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 match self {
176 Self::Unknown => write!(f, "unknown work"),
177 Self::Feature => write!(f, "feature development"),
178 Self::Fix => write!(f, "bug fix"),
179 Self::Docs => write!(f, "documentation update"),
180 Self::Refactor => write!(f, "refactoring"),
181 Self::Chore => write!(f, "maintenance"),
182 Self::Test => write!(f, "testing"),
183 Self::Ci => write!(f, "CI/CD"),
184 Self::Build => write!(f, "build system"),
185 Self::Perf => write!(f, "performance improvement"),
186 }
187 }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, Default)]
192pub struct CommitRangeContext {
193 pub related_commits: Vec<String>, pub common_files: Vec<PathBuf>,
197 pub work_pattern: WorkPattern,
199 pub scope_consistency: ScopeAnalysis,
201 pub architectural_impact: ArchitecturalImpact,
203 pub change_significance: ChangeSignificance,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, Default)]
209pub enum WorkPattern {
210 #[default]
211 Unknown,
213 Sequential,
215 Refactoring,
217 BugHunt,
219 Documentation,
221 Configuration,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize, Default)]
227pub struct ScopeAnalysis {
228 pub consistent_scope: Option<String>,
230 pub scope_changes: Vec<String>,
232 pub confidence: f32,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize, Default)]
238pub enum ArchitecturalImpact {
239 #[default]
240 Minimal,
242 Moderate,
244 Significant,
246 Breaking,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize, Default)]
252pub enum ChangeSignificance {
253 #[default]
254 Minor,
256 Moderate,
258 Major,
260 Critical,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct FileContext {
267 pub path: PathBuf,
269 pub file_purpose: FilePurpose,
271 pub architectural_layer: ArchitecturalLayer,
273 pub change_impact: ChangeImpact,
275 pub project_significance: ProjectSignificance,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
281pub enum FilePurpose {
282 Config,
284 Test,
286 Documentation,
288 CoreLogic,
290 Interface,
292 Build,
294 Tooling,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
300pub enum ArchitecturalLayer {
301 Presentation,
303 Business,
305 Data,
307 Infrastructure,
309 Cross,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
315pub enum ChangeImpact {
316 Style,
318 Additive,
320 Modification,
322 Breaking,
324 Critical,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
330pub enum ProjectSignificance {
331 Routine,
333 Important,
335 Critical,
337}
338
339impl CommitContext {
340 pub fn new() -> Self {
342 Self::default()
343 }
344
345 #[must_use]
347 pub fn is_significant_change(&self) -> bool {
348 matches!(
349 self.range.change_significance,
350 ChangeSignificance::Major | ChangeSignificance::Critical
351 ) || matches!(
352 self.range.architectural_impact,
353 ArchitecturalImpact::Significant | ArchitecturalImpact::Breaking
354 ) || self.files.iter().any(|f| {
355 matches!(f.project_significance, ProjectSignificance::Critical)
356 || matches!(
357 f.change_impact,
358 ChangeImpact::Breaking | ChangeImpact::Critical
359 )
360 })
361 }
362
363 pub fn suggested_verbosity(&self) -> VerbosityLevel {
365 if self.is_significant_change() {
366 VerbosityLevel::Comprehensive
367 } else if matches!(self.range.change_significance, ChangeSignificance::Moderate)
368 || self.files.len() > 1
369 || self.files.iter().any(|f| {
370 matches!(
371 f.architectural_layer,
372 ArchitecturalLayer::Presentation | ArchitecturalLayer::Business
373 )
374 })
375 {
376 VerbosityLevel::Detailed
381 } else {
382 VerbosityLevel::Concise
383 }
384 }
385}
386
387#[derive(Debug, Clone, Copy)]
389pub enum VerbosityLevel {
390 Concise,
392 Detailed,
394 Comprehensive,
396}
397
398#[cfg(test)]
399#[allow(clippy::unwrap_used, clippy::expect_used)]
400mod tests {
401 use super::*;
402 use std::str::FromStr;
403
404 #[test]
407 fn work_type_known_variants() {
408 assert!(matches!(
409 WorkType::from_str("feature").unwrap(),
410 WorkType::Feature
411 ));
412 assert!(matches!(
413 WorkType::from_str("feat").unwrap(),
414 WorkType::Feature
415 ));
416 assert!(matches!(WorkType::from_str("fix").unwrap(), WorkType::Fix));
417 assert!(matches!(
418 WorkType::from_str("bugfix").unwrap(),
419 WorkType::Fix
420 ));
421 assert!(matches!(
422 WorkType::from_str("docs").unwrap(),
423 WorkType::Docs
424 ));
425 assert!(matches!(WorkType::from_str("doc").unwrap(), WorkType::Docs));
426 assert!(matches!(
427 WorkType::from_str("refactor").unwrap(),
428 WorkType::Refactor
429 ));
430 assert!(matches!(
431 WorkType::from_str("chore").unwrap(),
432 WorkType::Chore
433 ));
434 assert!(matches!(
435 WorkType::from_str("test").unwrap(),
436 WorkType::Test
437 ));
438 assert!(matches!(WorkType::from_str("ci").unwrap(), WorkType::Ci));
439 assert!(matches!(
440 WorkType::from_str("build").unwrap(),
441 WorkType::Build
442 ));
443 assert!(matches!(
444 WorkType::from_str("perf").unwrap(),
445 WorkType::Perf
446 ));
447 }
448
449 #[test]
450 fn work_type_unknown() {
451 assert!(matches!(
452 WorkType::from_str("random").unwrap(),
453 WorkType::Unknown
454 ));
455 assert!(matches!(WorkType::from_str("").unwrap(), WorkType::Unknown));
456 }
457
458 #[test]
459 fn work_type_display() {
460 assert_eq!(WorkType::Feature.to_string(), "feature development");
461 assert_eq!(WorkType::Fix.to_string(), "bug fix");
462 assert_eq!(WorkType::Unknown.to_string(), "unknown work");
463 }
464
465 #[test]
468 fn significant_when_breaking_impact() {
469 let mut ctx = CommitContext::new();
470 ctx.range.architectural_impact = ArchitecturalImpact::Breaking;
471 assert!(ctx.is_significant_change());
472 }
473
474 #[test]
475 fn significant_when_critical_change() {
476 let mut ctx = CommitContext::new();
477 ctx.range.change_significance = ChangeSignificance::Critical;
478 assert!(ctx.is_significant_change());
479 }
480
481 #[test]
482 fn significant_when_critical_file() {
483 let mut ctx = CommitContext::new();
484 ctx.files.push(FileContext {
485 path: "src/main.rs".into(),
486 file_purpose: FilePurpose::CoreLogic,
487 architectural_layer: ArchitecturalLayer::Business,
488 change_impact: ChangeImpact::Breaking,
489 project_significance: ProjectSignificance::Critical,
490 });
491 assert!(ctx.is_significant_change());
492 }
493
494 #[test]
495 fn not_significant_when_minor() {
496 let ctx = CommitContext::new();
497 assert!(!ctx.is_significant_change());
498 }
499
500 #[test]
503 fn comprehensive_for_significant() {
504 let mut ctx = CommitContext::new();
505 ctx.range.architectural_impact = ArchitecturalImpact::Breaking;
506 assert!(matches!(
507 ctx.suggested_verbosity(),
508 VerbosityLevel::Comprehensive
509 ));
510 }
511
512 #[test]
513 fn detailed_for_moderate() {
514 let mut ctx = CommitContext::new();
515 ctx.range.change_significance = ChangeSignificance::Moderate;
516 assert!(matches!(
517 ctx.suggested_verbosity(),
518 VerbosityLevel::Detailed
519 ));
520 }
521
522 #[test]
523 fn concise_for_minor() {
524 let ctx = CommitContext::new();
525 assert!(matches!(ctx.suggested_verbosity(), VerbosityLevel::Concise));
526 }
527}