omni_dev/data/
mod.rs

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