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 generated_from: Vec<String>,
58    /// SHA-256 of the tracked raw bytes before decoding or newline normalization.
59    pub content_sha256: String,
60    pub text: String,
61    pub analysis_status: String,
62    pub skipped_reason: Option<String>,
63    pub symlink_metadata: Option<Value>,
64}
65
66#[derive(Debug, Clone, Default, Serialize, Deserialize)]
67pub struct HistoryMetrics {
68    pub first_seen_timestamp: Option<i64>,
69    pub age_days: u64,
70    pub revisions_window: usize,
71    pub recency_weighted_commits: f64,
72    pub added_window: usize,
73    pub deleted_window: usize,
74    pub line_churn_window: usize,
75    pub token_churn_window: usize,
76    pub relative_churn_window: f64,
77    pub late_churn_spike: f64,
78    pub author_count_window: usize,
79    pub author_entropy: f64,
80    pub top_author_share: f64,
81    pub days_since_non_bot_edit: Option<u64>,
82    pub recent_maintainer_diversity: usize,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct CommitRecord {
87    pub commit: String,
88    pub timestamp: i64,
89    pub author: String,
90    pub paths: Vec<String>,
91    pub line_churn_by_path: BTreeMap<String, usize>,
92    pub change_set_size: usize,
93    pub change_kind: String,
94    pub calibration_weight: f64,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(deny_unknown_fields)]
99pub struct FileAnalysis {
100    pub path: String,
101    pub bytes: usize,
102    pub lines: usize,
103    pub blank_lines: usize,
104    pub code_lines: usize,
105    pub comment_lines: usize,
106    pub language: String,
107    pub profile: String,
108    pub classification: String,
109    pub generated_from: Vec<String>,
110    pub analysis_status: String,
111    pub skipped_reason: Option<String>,
112    pub symlink_metadata: Option<Value>,
113    pub has_inline_tests: bool,
114    pub tokens: usize,
115    pub context_band: String,
116    pub context_pressure: f64,
117    /// Stable content identity used to distinguish source changes from
118    /// history-only score movement during report comparison.
119    pub content_fingerprint: String,
120    /// Exact tracked-content identity, including binary and otherwise skipped records.
121    pub content_sha256: String,
122    #[serde(skip)]
123    pub structural_tokens: Vec<String>,
124    pub structural_token_count: usize,
125    pub top_structural_terms: Vec<String>,
126    pub structural_categories: Value,
127    pub age_days: u64,
128    pub revisions_window: usize,
129    pub recency_weighted_commits: f64,
130    pub added_window: usize,
131    pub deleted_window: usize,
132    pub churn_lines_window: usize,
133    pub line_churn_window: usize,
134    pub token_churn_window: usize,
135    pub relative_churn_window: f64,
136    pub late_churn_spike: f64,
137    pub author_count_window: usize,
138    pub author_entropy: f64,
139    pub top_author_share: f64,
140    pub days_since_non_bot_edit: Option<u64>,
141    pub recent_maintainer_diversity: usize,
142    pub age_pressure: f64,
143    pub revision_norm: f64,
144    pub relative_churn_norm: f64,
145    pub churn_pressure: f64,
146    pub slop_score: f64,
147    pub slop_band: String,
148    pub reason_codes: Vec<String>,
149    pub costs: Value,
150    pub overlays: Value,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154#[serde(deny_unknown_fields)]
155pub struct FolderAnalysis {
156    pub path: String,
157    pub descendant_file_count: usize,
158    pub direct_file_count: usize,
159    pub bytes: usize,
160    pub lines: usize,
161    pub tokens: usize,
162    pub direct_tokens: usize,
163    pub context_band: String,
164    pub health_band: String,
165    pub context_pressure: f64,
166    pub slop_score: f64,
167    pub slop_band: String,
168    pub reason_codes: Vec<String>,
169    pub top_file_path: String,
170    pub classification: String,
171    pub costs: Value,
172    pub overlays: Value,
173}
174
175#[derive(Debug, Clone, Default, Serialize, Deserialize)]
176pub struct OrganizationAnalysis {
177    pub organization_metrics: Value,
178    pub relationships: Value,
179    pub clusters: Value,
180    pub file_overlays: BTreeMap<String, Value>,
181    pub folder_overlays: BTreeMap<String, Value>,
182    pub top_structural_files: Vec<Value>,
183}
184
185#[derive(Debug, Clone)]
186pub struct Analysis {
187    pub output_root: PathBuf,
188    pub report_profile: String,
189    pub compression: String,
190    pub repo: RepoMetadata,
191    pub config: Value,
192    pub generated_at: String,
193    pub analyzed_revision_at: Option<String>,
194    pub skipped: SkippedCounts,
195    pub tracked_file_count: usize,
196    pub scope: ScopeIdentity,
197    pub files: Vec<FileAnalysis>,
198    pub folders: Vec<FolderAnalysis>,
199    pub organization: OrganizationAnalysis,
200    pub action_queue: Vec<Value>,
201    pub observation_feed: Vec<Value>,
202    pub diagnostics: Value,
203}
204
205#[derive(Debug, Clone)]
206pub struct FindResult {
207    pub report: Value,
208    pub report_json: PathBuf,
209    /// Conventional YAML path. The file exists only when output.yaml is true.
210    pub report_yaml: PathBuf,
211    pub summary_md: PathBuf,
212    pub health_md: PathBuf,
213    pub compressed_report: Option<PathBuf>,
214    pub terminal: String,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
218#[serde(deny_unknown_fields)]
219pub struct Finding {
220    pub path: String,
221    pub profile: String,
222    pub severity: String,
223    pub title: String,
224    pub message: String,
225    pub next_command: String,
226    pub slop_band: String,
227    pub context_band: String,
228    pub slop_score: f64,
229    pub tokens: usize,
230    pub reasons: Vec<String>,
231}
232
233#[derive(Debug, Clone, Default, Serialize, Deserialize)]
234#[serde(deny_unknown_fields)]
235pub struct HealthRollup {
236    pub file_band_counts: BTreeMap<String, usize>,
237    pub folder_band_counts: BTreeMap<String, usize>,
238    pub profile_rollups: Vec<Value>,
239    pub language_rollups: BTreeMap<String, Vec<Value>>,
240    pub file_distribution: Value,
241    pub folder_distribution: Value,
242    pub refactor_candidates: Vec<Value>,
243    pub watchlist: Vec<Value>,
244    pub findings: Vec<Finding>,
245}
246
247pub fn parent_folders(path: &str) -> Vec<String> {
248    let normalized = path.trim_matches('/');
249    let mut result = vec![".".to_string()];
250    let parts: Vec<&str> = normalized.split('/').collect();
251    if parts.len() <= 1 {
252        return result;
253    }
254    for index in 1..parts.len() {
255        result.push(parts[..index].join("/"));
256    }
257    result
258}
259
260pub fn top_level_root(path: &str) -> String {
261    path.trim_matches('/')
262        .split('/')
263        .next()
264        .filter(|part| !part.is_empty())
265        .unwrap_or(".")
266        .to_string()
267}