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