Skip to main content

omni_dev/data/
mod.rs

1//! Data processing and serialization
2
3use crate::git::{CommitInfo, CommitInfoForAI, RemoteInfo};
4use serde::{Deserialize, Serialize};
5
6pub mod amendments;
7pub mod check;
8pub mod context;
9pub mod yaml;
10
11pub use amendments::*;
12pub use check::*;
13pub use context::*;
14pub use yaml::*;
15
16/// Complete repository view output structure
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct RepositoryView {
19    /// Version information for the omni-dev tool
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub versions: Option<VersionInfo>,
22    /// Explanation of field meanings and structure
23    pub explanation: FieldExplanation,
24    /// Working directory status information
25    pub working_directory: WorkingDirectoryInfo,
26    /// List of remote repositories and their main branches
27    pub remotes: Vec<RemoteInfo>,
28    /// AI-related information
29    pub ai: AiInfo,
30    /// Branch information (only present when using branch commands)
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub branch_info: Option<BranchInfo>,
33    /// Pull request template content (only present in branch commands when template exists)
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub pr_template: Option<String>,
36    /// Location of the pull request template file (only present when pr_template exists)
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub pr_template_location: Option<String>,
39    /// Pull requests created from the current branch (only present in branch commands)
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub branch_prs: Option<Vec<PullRequest>>,
42    /// List of analyzed commits with metadata and analysis
43    pub commits: Vec<CommitInfo>,
44}
45
46/// Enhanced repository view for AI processing with full diff content
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct RepositoryViewForAI {
49    /// Version information for the omni-dev tool
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub versions: Option<VersionInfo>,
52    /// Explanation of field meanings and structure
53    pub explanation: FieldExplanation,
54    /// Working directory status information
55    pub working_directory: WorkingDirectoryInfo,
56    /// List of remote repositories and their main branches
57    pub remotes: Vec<RemoteInfo>,
58    /// AI-related information
59    pub ai: AiInfo,
60    /// Branch information (only present when using branch commands)
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub branch_info: Option<BranchInfo>,
63    /// Pull request template content (only present in branch commands when template exists)
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub pr_template: Option<String>,
66    /// Location of the pull request template file (only present when pr_template exists)
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub pr_template_location: Option<String>,
69    /// Pull requests created from the current branch (only present in branch commands)
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub branch_prs: Option<Vec<PullRequest>>,
72    /// List of analyzed commits with enhanced metadata including full diff content
73    pub commits: Vec<CommitInfoForAI>,
74}
75
76/// Field explanation for the YAML output
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct FieldExplanation {
79    /// Descriptive text explaining the overall structure
80    pub text: String,
81    /// Documentation for individual fields in the output
82    pub fields: Vec<FieldDocumentation>,
83}
84
85/// Individual field documentation
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct FieldDocumentation {
88    /// Name of the field being documented
89    pub name: String,
90    /// Descriptive text explaining what the field contains
91    pub text: String,
92    /// Git command that corresponds to this field (if applicable)
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub command: Option<String>,
95    /// Whether this field is present in the current output
96    pub present: bool,
97}
98
99/// Working directory information
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct WorkingDirectoryInfo {
102    /// Whether the working directory has no changes
103    pub clean: bool,
104    /// List of files with uncommitted changes
105    pub untracked_changes: Vec<FileStatusInfo>,
106}
107
108/// File status information for working directory
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct FileStatusInfo {
111    /// Git status flags (e.g., "AM", "??", "M ")
112    pub status: String,
113    /// Path to the file relative to repository root
114    pub file: String,
115}
116
117/// Version information for tools and environment
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct VersionInfo {
120    /// Version of the omni-dev tool
121    pub omni_dev: String,
122}
123
124/// AI-related information
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct AiInfo {
127    /// Path to AI scratch directory
128    pub scratch: String,
129}
130
131/// Branch information for branch-specific commands
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct BranchInfo {
134    /// Current branch name
135    pub branch: String,
136}
137
138/// Pull request information
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct PullRequest {
141    /// PR number
142    pub number: u64,
143    /// PR title
144    pub title: String,
145    /// PR state (open, closed, merged)
146    pub state: String,
147    /// PR URL
148    pub url: String,
149    /// PR description/body content
150    pub body: String,
151    /// Base branch the PR targets
152    #[serde(default)]
153    pub base: String,
154}
155
156impl RepositoryView {
157    /// Update the present field for all field documentation entries based on actual data
158    pub fn update_field_presence(&mut self) {
159        for field in &mut self.explanation.fields {
160            field.present = match field.name.as_str() {
161                "working_directory.clean" => true,             // Always present
162                "working_directory.untracked_changes" => true, // Always present
163                "remotes" => true,                             // Always present
164                "commits[].hash" => !self.commits.is_empty(),
165                "commits[].author" => !self.commits.is_empty(),
166                "commits[].date" => !self.commits.is_empty(),
167                "commits[].original_message" => !self.commits.is_empty(),
168                "commits[].in_main_branches" => !self.commits.is_empty(),
169                "commits[].analysis.detected_type" => !self.commits.is_empty(),
170                "commits[].analysis.detected_scope" => !self.commits.is_empty(),
171                "commits[].analysis.proposed_message" => !self.commits.is_empty(),
172                "commits[].analysis.file_changes.total_files" => !self.commits.is_empty(),
173                "commits[].analysis.file_changes.files_added" => !self.commits.is_empty(),
174                "commits[].analysis.file_changes.files_deleted" => !self.commits.is_empty(),
175                "commits[].analysis.file_changes.file_list" => !self.commits.is_empty(),
176                "commits[].analysis.diff_summary" => !self.commits.is_empty(),
177                "commits[].analysis.diff_file" => !self.commits.is_empty(),
178                "versions.omni_dev" => self.versions.is_some(),
179                "ai.scratch" => true,
180                "branch_info.branch" => self.branch_info.is_some(),
181                "pr_template" => self.pr_template.is_some(),
182                "pr_template_location" => self.pr_template_location.is_some(),
183                "branch_prs" => self.branch_prs.is_some(),
184                "branch_prs[].number" => {
185                    self.branch_prs.as_ref().is_some_and(|prs| !prs.is_empty())
186                }
187                "branch_prs[].title" => self.branch_prs.as_ref().is_some_and(|prs| !prs.is_empty()),
188                "branch_prs[].state" => self.branch_prs.as_ref().is_some_and(|prs| !prs.is_empty()),
189                "branch_prs[].url" => self.branch_prs.as_ref().is_some_and(|prs| !prs.is_empty()),
190                "branch_prs[].body" => self.branch_prs.as_ref().is_some_and(|prs| !prs.is_empty()),
191                _ => false, // Unknown fields are not present
192            }
193        }
194    }
195}
196
197impl Default for FieldExplanation {
198    /// Create default field explanation
199    fn default() -> Self {
200        Self {
201            text: [
202                "Field documentation for the YAML output format. Each entry describes the purpose and content of fields returned by the view command.",
203                "",
204                "Field structure:",
205                "- name: Specifies the YAML field path",
206                "- text: Provides a description of what the field contains",
207                "- command: Shows the corresponding command used to obtain that data (if applicable)",
208                "- present: Indicates whether this field is present in the current output",
209                "",
210                "IMPORTANT FOR AI ASSISTANTS: If a field shows present=true, it is guaranteed to be somewhere in this document. AI assistants should search the entire document thoroughly for any field marked as present=true, as it is definitely included in the output."
211            ].join("\n"),
212            fields: vec![
213                FieldDocumentation {
214                    name: "working_directory.clean".to_string(),
215                    text: "Boolean indicating if the working directory has no uncommitted changes".to_string(),
216                    command: Some("git status".to_string()),
217                    present: false, // Will be set dynamically when creating output
218                },
219                FieldDocumentation {
220                    name: "working_directory.untracked_changes".to_string(),
221                    text: "Array of files with uncommitted changes, showing git status and file path".to_string(),
222                    command: Some("git status --porcelain".to_string()),
223                    present: false,
224                },
225                FieldDocumentation {
226                    name: "remotes".to_string(),
227                    text: "Array of git remotes with their URLs and detected main branch names".to_string(),
228                    command: Some("git remote -v".to_string()),
229                    present: false,
230                },
231                FieldDocumentation {
232                    name: "commits[].hash".to_string(),
233                    text: "Full SHA-1 hash of the commit".to_string(),
234                    command: Some("git log --format=%H".to_string()),
235                    present: false,
236                },
237                FieldDocumentation {
238                    name: "commits[].author".to_string(),
239                    text: "Commit author name and email address".to_string(),
240                    command: Some("git log --format=%an <%ae>".to_string()),
241                    present: false,
242                },
243                FieldDocumentation {
244                    name: "commits[].date".to_string(),
245                    text: "Commit date in ISO format with timezone".to_string(),
246                    command: Some("git log --format=%aI".to_string()),
247                    present: false,
248                },
249                FieldDocumentation {
250                    name: "commits[].original_message".to_string(),
251                    text: "The original commit message as written by the author".to_string(),
252                    command: Some("git log --format=%B".to_string()),
253                    present: false,
254                },
255                FieldDocumentation {
256                    name: "commits[].in_main_branches".to_string(),
257                    text: "Array of remote main branches that contain this commit (empty if not pushed)".to_string(),
258                    command: Some("git branch -r --contains <commit>".to_string()),
259                    present: false,
260                },
261                FieldDocumentation {
262                    name: "commits[].analysis.detected_type".to_string(),
263                    text: "Automatically detected conventional commit type (feat, fix, docs, test, chore, etc.)".to_string(),
264                    command: None,
265                    present: false,
266                },
267                FieldDocumentation {
268                    name: "commits[].analysis.detected_scope".to_string(),
269                    text: "Automatically detected scope based on file paths (commands, config, tests, etc.)".to_string(),
270                    command: None,
271                    present: false,
272                },
273                FieldDocumentation {
274                    name: "commits[].analysis.proposed_message".to_string(),
275                    text: "AI-generated conventional commit message based on file changes".to_string(),
276                    command: None,
277                    present: false,
278                },
279                FieldDocumentation {
280                    name: "commits[].analysis.file_changes.total_files".to_string(),
281                    text: "Total number of files modified in this commit".to_string(),
282                    command: Some("git show --name-only <commit>".to_string()),
283                    present: false,
284                },
285                FieldDocumentation {
286                    name: "commits[].analysis.file_changes.files_added".to_string(),
287                    text: "Number of new files added in this commit".to_string(),
288                    command: Some("git show --name-status <commit> | grep '^A'".to_string()),
289                    present: false,
290                },
291                FieldDocumentation {
292                    name: "commits[].analysis.file_changes.files_deleted".to_string(),
293                    text: "Number of files deleted in this commit".to_string(),
294                    command: Some("git show --name-status <commit> | grep '^D'".to_string()),
295                    present: false,
296                },
297                FieldDocumentation {
298                    name: "commits[].analysis.file_changes.file_list".to_string(),
299                    text: "Array of files changed with their git status (M=modified, A=added, D=deleted)".to_string(),
300                    command: Some("git show --name-status <commit>".to_string()),
301                    present: false,
302                },
303                FieldDocumentation {
304                    name: "commits[].analysis.diff_summary".to_string(),
305                    text: "Git diff --stat output showing lines changed per file".to_string(),
306                    command: Some("git show --stat <commit>".to_string()),
307                    present: false,
308                },
309                FieldDocumentation {
310                    name: "commits[].analysis.diff_file".to_string(),
311                    text: "Path to file containing full diff content showing line-by-line changes with added, removed, and context lines.\n\
312                           AI assistants should read this file to understand the specific changes made in the commit.".to_string(),
313                    command: Some("git show <commit>".to_string()),
314                    present: false,
315                },
316                FieldDocumentation {
317                    name: "versions.omni_dev".to_string(),
318                    text: "Version of the omni-dev tool".to_string(),
319                    command: Some("omni-dev --version".to_string()),
320                    present: false,
321                },
322                FieldDocumentation {
323                    name: "ai.scratch".to_string(),
324                    text: "Path to AI scratch directory (controlled by AI_SCRATCH environment variable)".to_string(),
325                    command: Some("echo $AI_SCRATCH".to_string()),
326                    present: false,
327                },
328                FieldDocumentation {
329                    name: "branch_info.branch".to_string(),
330                    text: "Current branch name (only present in branch commands)".to_string(),
331                    command: Some("git branch --show-current".to_string()),
332                    present: false,
333                },
334                FieldDocumentation {
335                    name: "pr_template".to_string(),
336                    text: "Pull request template content from .github/pull_request_template.md (only present in branch commands when file exists)".to_string(),
337                    command: None,
338                    present: false,
339                },
340                FieldDocumentation {
341                    name: "pr_template_location".to_string(),
342                    text: "Location of the pull request template file (only present when pr_template exists)".to_string(),
343                    command: None,
344                    present: false,
345                },
346                FieldDocumentation {
347                    name: "branch_prs".to_string(),
348                    text: "Pull requests created from the current branch (only present in branch commands)".to_string(),
349                    command: None,
350                    present: false,
351                },
352                FieldDocumentation {
353                    name: "branch_prs[].number".to_string(),
354                    text: "Pull request number".to_string(),
355                    command: None,
356                    present: false,
357                },
358                FieldDocumentation {
359                    name: "branch_prs[].title".to_string(),
360                    text: "Pull request title".to_string(),
361                    command: None,
362                    present: false,
363                },
364                FieldDocumentation {
365                    name: "branch_prs[].state".to_string(),
366                    text: "Pull request state (open, closed, merged)".to_string(),
367                    command: None,
368                    present: false,
369                },
370                FieldDocumentation {
371                    name: "branch_prs[].url".to_string(),
372                    text: "Pull request URL".to_string(),
373                    command: None,
374                    present: false,
375                },
376                FieldDocumentation {
377                    name: "branch_prs[].body".to_string(),
378                    text: "Pull request description/body content".to_string(),
379                    command: None,
380                    present: false,
381                },
382            ],
383        }
384    }
385}
386
387impl RepositoryViewForAI {
388    /// Convert from basic RepositoryView by loading diff content for all commits
389    pub fn from_repository_view(repo_view: RepositoryView) -> anyhow::Result<Self> {
390        Self::from_repository_view_with_options(repo_view, false)
391    }
392
393    /// Convert from basic RepositoryView with options
394    ///
395    /// If `fresh` is true, clears original commit messages to force AI to generate
396    /// new messages based solely on the diff content.
397    pub fn from_repository_view_with_options(
398        repo_view: RepositoryView,
399        fresh: bool,
400    ) -> anyhow::Result<Self> {
401        // Convert all commits to AI-enhanced versions
402        let commits: anyhow::Result<Vec<_>> = repo_view
403            .commits
404            .into_iter()
405            .map(|commit| {
406                let mut ai_commit = CommitInfoForAI::from_commit_info(commit)?;
407                if fresh {
408                    ai_commit.original_message =
409                        "(Original message hidden - generate fresh message from diff)".to_string();
410                }
411                Ok(ai_commit)
412            })
413            .collect();
414
415        Ok(Self {
416            versions: repo_view.versions,
417            explanation: repo_view.explanation,
418            working_directory: repo_view.working_directory,
419            remotes: repo_view.remotes,
420            ai: repo_view.ai,
421            branch_info: repo_view.branch_info,
422            pr_template: repo_view.pr_template,
423            pr_template_location: repo_view.pr_template_location,
424            branch_prs: repo_view.branch_prs,
425            commits: commits?,
426        })
427    }
428}