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