Skip to main content

git_slop/
model.rs

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