Skip to main content

git_slop/
model.rs

1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct RepoMetadata {
9    pub repo_name: String,
10    pub repo_root: String,
11    pub branch: Option<String>,
12    pub head_commit: Option<String>,
13    pub head_commit_timestamp: Option<String>,
14    pub git_remote_url: Option<String>,
15    pub is_shallow: bool,
16    pub detached_head: bool,
17    pub worktree_clean: bool,
18    pub staged_change_count: usize,
19    pub modified_tracked_file_count: usize,
20    pub untracked_file_count: usize,
21    pub worktree_state_digest: String,
22    pub analyzed_content_digest: Option<String>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26pub struct ScopeIdentity {
27    pub mode: String,
28    pub path: Option<String>,
29    pub selected_path_count: usize,
30    pub selected_path_digest: String,
31}
32
33#[derive(Debug, Clone, Default, Serialize, Deserialize)]
34pub struct SkippedCounts {
35    pub ignored: usize,
36    pub missing: usize,
37    pub binary: usize,
38    pub undecodable: usize,
39}
40
41#[derive(Debug, Clone)]
42pub struct InventoryFile {
43    pub path: String,
44    pub bytes: usize,
45    pub lines: usize,
46    pub blank_lines: usize,
47    pub code_lines: usize,
48    pub comment_lines: usize,
49    pub language: String,
50    pub profile: String,
51    pub classification: String,
52    pub text: String,
53}
54
55#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct HistoryMetrics {
57    pub first_seen_timestamp: Option<i64>,
58    pub age_days: u64,
59    pub revisions_window: usize,
60    pub recency_weighted_commits: f64,
61    pub added_window: usize,
62    pub deleted_window: usize,
63    pub line_churn_window: usize,
64    pub token_churn_window: usize,
65    pub relative_churn_window: f64,
66    pub late_churn_spike: f64,
67    pub author_count_window: usize,
68    pub author_entropy: f64,
69    pub top_author_share: f64,
70    pub days_since_non_bot_edit: Option<u64>,
71    pub recent_maintainer_diversity: usize,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct CommitRecord {
76    pub commit: String,
77    pub timestamp: i64,
78    pub author: String,
79    pub paths: Vec<String>,
80    pub line_churn_by_path: BTreeMap<String, usize>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct FileAnalysis {
85    pub path: String,
86    pub bytes: usize,
87    pub lines: usize,
88    pub blank_lines: usize,
89    pub code_lines: usize,
90    pub comment_lines: usize,
91    pub language: String,
92    pub profile: String,
93    pub classification: String,
94    pub has_inline_tests: bool,
95    pub tokens: usize,
96    pub context_band: String,
97    pub context_pressure: f64,
98    /// Stable content identity used to distinguish source changes from
99    /// history-only score movement during report comparison.
100    pub content_fingerprint: String,
101    #[serde(skip)]
102    pub structural_tokens: Vec<String>,
103    pub structural_token_count: usize,
104    pub top_structural_terms: Vec<String>,
105    pub age_days: u64,
106    pub revisions_window: usize,
107    pub recency_weighted_commits: f64,
108    pub added_window: usize,
109    pub deleted_window: usize,
110    pub churn_lines_window: usize,
111    pub line_churn_window: usize,
112    pub token_churn_window: usize,
113    pub relative_churn_window: f64,
114    pub late_churn_spike: f64,
115    pub author_count_window: usize,
116    pub author_entropy: f64,
117    pub top_author_share: f64,
118    pub days_since_non_bot_edit: Option<u64>,
119    pub recent_maintainer_diversity: usize,
120    pub age_pressure: f64,
121    pub revision_norm: f64,
122    pub relative_churn_norm: f64,
123    pub churn_pressure: f64,
124    pub slop_score: f64,
125    pub slop_band: String,
126    pub reason_codes: Vec<String>,
127    pub costs: Value,
128    pub overlays: Value,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct FolderAnalysis {
133    pub path: String,
134    pub descendant_file_count: usize,
135    pub direct_file_count: usize,
136    pub bytes: usize,
137    pub lines: usize,
138    pub tokens: usize,
139    pub direct_tokens: usize,
140    pub context_band: String,
141    pub health_band: String,
142    pub context_pressure: f64,
143    pub slop_score: f64,
144    pub slop_band: String,
145    pub reason_codes: Vec<String>,
146    pub top_file_path: String,
147    pub classification: String,
148    pub costs: Value,
149    pub overlays: Value,
150}
151
152#[derive(Debug, Clone, Default, Serialize, Deserialize)]
153pub struct OrganizationAnalysis {
154    pub organization_metrics: Value,
155    pub relationships: Value,
156    pub clusters: Value,
157    pub file_overlays: BTreeMap<String, Value>,
158    pub folder_overlays: BTreeMap<String, Value>,
159    pub top_structural_files: Vec<Value>,
160}
161
162#[derive(Debug, Clone)]
163pub struct Analysis {
164    pub repo_root: PathBuf,
165    pub repo: RepoMetadata,
166    pub config: Value,
167    pub generated_at: String,
168    pub analyzed_revision_at: Option<String>,
169    pub skipped: SkippedCounts,
170    pub tracked_file_count: usize,
171    pub scope: ScopeIdentity,
172    pub files: Vec<FileAnalysis>,
173    pub folders: Vec<FolderAnalysis>,
174    pub organization: OrganizationAnalysis,
175    pub action_queue: Vec<Value>,
176    pub diagnostics: Value,
177}
178
179#[derive(Debug, Clone)]
180pub struct FindResult {
181    pub report: Value,
182    pub report_json: PathBuf,
183    /// Conventional YAML path. The file exists only when output.yaml is true.
184    pub report_yaml: PathBuf,
185    pub summary_md: PathBuf,
186    pub health_md: PathBuf,
187    pub terminal: String,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct Finding {
192    pub path: String,
193    pub profile: String,
194    pub severity: String,
195    pub title: String,
196    pub message: String,
197    pub next_command: String,
198    pub slop_band: String,
199    pub context_band: String,
200    pub slop_score: f64,
201    pub tokens: usize,
202    pub reasons: Vec<String>,
203}
204
205#[derive(Debug, Clone, Default, Serialize, Deserialize)]
206pub struct HealthRollup {
207    pub file_band_counts: BTreeMap<String, usize>,
208    pub folder_band_counts: BTreeMap<String, usize>,
209    pub profile_rollups: Vec<Value>,
210    pub language_rollups: BTreeMap<String, Vec<Value>>,
211    pub file_distribution: Value,
212    pub folder_distribution: Value,
213    pub refactor_candidates: Vec<Value>,
214    pub watchlist: Vec<Value>,
215    pub findings: Vec<Finding>,
216}
217
218pub fn parent_folders(path: &str) -> Vec<String> {
219    let normalized = path.trim_matches('/');
220    let mut result = vec![".".to_string()];
221    let parts: Vec<&str> = normalized.split('/').collect();
222    if parts.len() <= 1 {
223        return result;
224    }
225    for index in 1..parts.len() {
226        result.push(parts[..index].join("/"));
227    }
228    result
229}
230
231pub fn top_level_root(path: &str) -> String {
232    path.trim_matches('/')
233        .split('/')
234        .next()
235        .filter(|part| !part.is_empty())
236        .unwrap_or(".")
237        .to_string()
238}