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