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}