Skip to main content

omni_dev/data/
context.rs

1//! Context data structures for enhanced commit message analysis.
2
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8
9/// Complete context information for intelligent commit message improvement.
10#[derive(Debug, Clone, Serialize, Deserialize, Default)]
11pub struct CommitContext {
12    /// Project-level context and conventions.
13    pub project: ProjectContext,
14    /// Branch analysis and work pattern detection.
15    pub branch: BranchContext,
16    /// Multi-commit analysis and work patterns.
17    pub range: CommitRangeContext,
18    /// File-specific context and architectural understanding.
19    pub files: Vec<FileContext>,
20    /// User-provided context information.
21    pub user_provided: Option<String>,
22}
23
24/// Project-level context discovered from configuration files.
25#[derive(Debug, Clone, Serialize, Deserialize, Default)]
26pub struct ProjectContext {
27    /// Project-specific commit guidelines from .omni-dev/commit-guidelines.md.
28    pub commit_guidelines: Option<String>,
29    /// Project-specific PR guidelines from .omni-dev/pr-guidelines.md.
30    pub pr_guidelines: Option<String>,
31    /// Valid scopes and their descriptions from .omni-dev/scopes.yaml.
32    pub valid_scopes: Vec<ScopeDefinition>,
33    /// Feature-specific context from .omni-dev/context/.
34    pub feature_contexts: HashMap<String, FeatureContext>,
35    /// Parsed conventions from CONTRIBUTING.md.
36    pub project_conventions: ProjectConventions,
37    /// Detected ecosystem (rust, node, python, etc.).
38    pub ecosystem: Ecosystem,
39}
40
41/// Definition of a valid scope in the project.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ScopeDefinition {
44    /// Name of the scope.
45    pub name: String,
46    /// Human-readable description of the scope.
47    pub description: String,
48    /// Example commit messages using this scope.
49    pub examples: Vec<String>,
50    /// File patterns that match this scope.
51    pub file_patterns: Vec<String>,
52}
53
54/// Context for a specific feature or work area.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct FeatureContext {
57    /// Name of the feature context.
58    pub name: String,
59    /// Description of the feature or work area.
60    pub description: String,
61    /// Associated scope for this feature.
62    pub scope: String,
63    /// Specific conventions for this feature.
64    pub conventions: Vec<String>,
65}
66
67/// Project conventions parsed from documentation.
68#[derive(Debug, Clone, Serialize, Deserialize, Default)]
69pub struct ProjectConventions {
70    /// Expected commit message format.
71    pub commit_format: Option<String>,
72    /// Required trailers like Signed-off-by.
73    pub required_trailers: Vec<String>,
74    /// Preferred commit types for this project.
75    pub preferred_types: Vec<String>,
76    /// Scope usage requirements and definitions.
77    pub scope_requirements: ScopeRequirements,
78}
79
80/// Requirements and validation rules for commit scopes.
81#[derive(Debug, Clone, Serialize, Deserialize, Default)]
82pub struct ScopeRequirements {
83    /// Whether a scope is required in commit messages.
84    pub required: bool,
85    /// List of valid scope names.
86    pub valid_scopes: Vec<String>,
87    /// Mapping from file patterns to suggested scopes.
88    pub scope_mapping: HashMap<String, Vec<String>>, // file patterns -> scope
89}
90
91/// Detected project ecosystem.
92#[derive(Debug, Clone, Serialize, Deserialize, Default)]
93pub enum Ecosystem {
94    #[default]
95    /// Unknown or undetected ecosystem.
96    Unknown,
97    /// Rust ecosystem with Cargo.
98    Rust,
99    /// Node.js ecosystem with npm/yarn.
100    Node,
101    /// Python ecosystem with pip/poetry.
102    Python,
103    /// Go ecosystem with go modules.
104    Go,
105    /// Java ecosystem with Maven/Gradle.
106    Java,
107    /// Generic project without specific ecosystem.
108    Generic,
109}
110
111/// Branch analysis and work pattern detection.
112#[derive(Debug, Clone, Serialize, Deserialize, Default)]
113pub struct BranchContext {
114    /// Type of work being performed on this branch.
115    pub work_type: WorkType,
116    /// Extracted scope from branch name.
117    pub scope: Option<String>,
118    /// Associated ticket or issue ID.
119    pub ticket_id: Option<String>,
120    /// Human-readable description of the branch purpose.
121    pub description: String,
122    /// Whether this is a feature branch (vs main/master).
123    pub is_feature_branch: bool,
124    /// Base branch this was created from.
125    pub base_branch: Option<String>,
126}
127
128/// Type of work being performed.
129#[derive(Debug, Clone, Serialize, Deserialize, Default)]
130pub enum WorkType {
131    #[default]
132    /// Unknown or unspecified work type.
133    Unknown,
134    /// New feature development.
135    Feature,
136    /// Bug fix.
137    Fix,
138    /// Documentation changes.
139    Docs,
140    /// Code refactoring.
141    Refactor,
142    /// Maintenance tasks.
143    Chore,
144    /// Test-related changes.
145    Test,
146    /// CI/CD pipeline changes.
147    Ci,
148    /// Build system changes.
149    Build,
150    /// Performance improvements.
151    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/// Multi-commit analysis and work patterns.
191#[derive(Debug, Clone, Serialize, Deserialize, Default)]
192pub struct CommitRangeContext {
193    /// Related commit hashes in this analysis.
194    pub related_commits: Vec<String>, // commit hashes
195    /// Files that appear in multiple commits.
196    pub common_files: Vec<PathBuf>,
197    /// Detected work pattern across commits.
198    pub work_pattern: WorkPattern,
199    /// Analysis of scope consistency.
200    pub scope_consistency: ScopeAnalysis,
201    /// Overall architectural impact assessment.
202    pub architectural_impact: ArchitecturalImpact,
203    /// Significance of changes for commit message detail.
204    pub change_significance: ChangeSignificance,
205}
206
207/// Detected work pattern across commits.
208#[derive(Debug, Clone, Serialize, Deserialize, Default)]
209pub enum WorkPattern {
210    #[default]
211    /// Unknown or undetected pattern.
212    Unknown,
213    /// Building feature step by step.
214    Sequential,
215    /// Multiple small cleanup commits.
216    Refactoring,
217    /// Investigation and fixes.
218    BugHunt,
219    /// Documentation updates.
220    Documentation,
221    /// Config and setup changes.
222    Configuration,
223}
224
225/// Analysis of scope consistency across commits.
226#[derive(Debug, Clone, Serialize, Deserialize, Default)]
227pub struct ScopeAnalysis {
228    /// Most consistent scope across commits if any.
229    pub consistent_scope: Option<String>,
230    /// All scope changes detected.
231    pub scope_changes: Vec<String>,
232    /// Confidence level in scope consistency (0.0-1.0).
233    pub confidence: f32,
234}
235
236/// Impact on system architecture.
237#[derive(Debug, Clone, Serialize, Deserialize, Default)]
238pub enum ArchitecturalImpact {
239    #[default]
240    /// Small changes, no architecture impact.
241    Minimal,
242    /// Some architectural changes.
243    Moderate,
244    /// Major architectural changes.
245    Significant,
246    /// Breaking changes.
247    Breaking,
248}
249
250/// Significance of changes for commit message detail level.
251#[derive(Debug, Clone, Serialize, Deserialize, Default)]
252pub enum ChangeSignificance {
253    #[default]
254    /// Simple fix or small addition.
255    Minor,
256    /// Notable feature or improvement.
257    Moderate,
258    /// Significant enhancement or new capability.
259    Major,
260    /// Major system changes or breaking changes.
261    Critical,
262}
263
264/// File-based context and architectural understanding.
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct FileContext {
267    /// Path to the file.
268    pub path: PathBuf,
269    /// Purpose of this file in the project.
270    pub file_purpose: FilePurpose,
271    /// Architectural layer this file belongs to.
272    pub architectural_layer: ArchitecturalLayer,
273    /// Impact of changes to this file.
274    pub change_impact: ChangeImpact,
275    /// Significance of this file in the project.
276    pub project_significance: ProjectSignificance,
277}
278
279/// Purpose of the file in the project.
280#[derive(Debug, Clone, Serialize, Deserialize)]
281pub enum FilePurpose {
282    /// Configuration files.
283    Config,
284    /// Test files.
285    Test,
286    /// Docs and README files.
287    Documentation,
288    /// Main application logic.
289    CoreLogic,
290    /// API definitions, public interfaces.
291    Interface,
292    /// Build and deployment files.
293    Build,
294    /// Development tools and scripts.
295    Tooling,
296}
297
298/// Architectural layer of the file.
299#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
300pub enum ArchitecturalLayer {
301    /// UI, CLI, web interfaces.
302    Presentation,
303    /// Core business logic.
304    Business,
305    /// Data access, models, storage.
306    Data,
307    /// System, network, deployment.
308    Infrastructure,
309    /// Cross-cutting concerns (logging, auth, etc.).
310    Cross,
311}
312
313/// Impact of changes to this file.
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub enum ChangeImpact {
316    /// Formatting, comments, style changes.
317    Style,
318    /// New functionality, non-breaking.
319    Additive,
320    /// Changing existing functionality.
321    Modification,
322    /// Breaking existing functionality.
323    Breaking,
324    /// Security, safety, or critical fixes.
325    Critical,
326}
327
328/// Significance of file in project.
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub enum ProjectSignificance {
331    /// Common, everyday files.
332    Routine,
333    /// Key files but not critical.
334    Important,
335    /// Core files that define the project.
336    Critical,
337}
338
339impl CommitContext {
340    /// Creates a new empty context.
341    pub fn new() -> Self {
342        Self::default()
343    }
344
345    /// Checks if this context suggests a significant change needing detailed commit message.
346    #[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    /// Returns the suggested commit message verbosity level.
364    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            // Be more generous with Detailed level for twiddle system:
377            // - Moderate changes
378            // - Multiple files
379            // - UI/CLI or business logic changes
380            VerbosityLevel::Detailed
381        } else {
382            VerbosityLevel::Concise
383        }
384    }
385}
386
387/// Suggested level of detail for commit messages.
388#[derive(Debug, Clone, Copy)]
389pub enum VerbosityLevel {
390    /// Single line, basic conventional commit.
391    Concise,
392    /// Subject + brief body paragraph.
393    Detailed,
394    /// Subject + detailed multi-paragraph body + lists.
395    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    // ── WorkType::from_str ───────────────────────────────────────────
405
406    #[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    // ── CommitContext::is_significant_change ──────────────────────────
466
467    #[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    // ── CommitContext::suggested_verbosity ────────────────────────────
501
502    #[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}