1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub(crate) enum Classification {
10 Source,
11 Test,
12 Docs,
13 Tool,
14 Workflow,
15 Config,
16 Data,
17 Generated,
18 Snapshot,
19 Fixture,
20 Vendored,
21 MigrationFixture,
22 Other,
23}
24
25impl Classification {
26 pub(crate) const ALL: [Self; 13] = [
27 Self::Source,
28 Self::Test,
29 Self::Docs,
30 Self::Tool,
31 Self::Workflow,
32 Self::Config,
33 Self::Data,
34 Self::Generated,
35 Self::Snapshot,
36 Self::Fixture,
37 Self::Vendored,
38 Self::MigrationFixture,
39 Self::Other,
40 ];
41
42 pub(crate) const fn as_str(self) -> &'static str {
43 match self {
44 Self::Source => "source",
45 Self::Test => "test",
46 Self::Docs => "docs",
47 Self::Tool => "tool",
48 Self::Workflow => "workflow",
49 Self::Config => "config",
50 Self::Data => "data",
51 Self::Generated => "generated",
52 Self::Snapshot => "snapshot",
53 Self::Fixture => "fixture",
54 Self::Vendored => "vendored",
55 Self::MigrationFixture => "migration_fixture",
56 Self::Other => "other",
57 }
58 }
59
60 pub(crate) fn is_valid(value: &str) -> bool {
61 Self::ALL
62 .iter()
63 .any(|candidate| candidate.as_str() == value)
64 }
65
66 pub(crate) fn values() -> Vec<&'static str> {
67 Self::ALL.iter().map(|value| value.as_str()).collect()
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct RepoMetadata {
73 pub repo_name: String,
74 pub repo_root: String,
75 pub repository_id: Option<String>,
78 pub repository_identity_source: Option<String>,
79 pub branch: Option<String>,
80 pub head_commit: Option<String>,
81 pub head_commit_timestamp: Option<String>,
82 pub git_remote_url: Option<String>,
83 pub is_shallow: bool,
84 pub detached_head: bool,
85 pub worktree_clean: bool,
86 pub staged_change_count: usize,
87 pub modified_tracked_file_count: usize,
88 pub untracked_file_count: usize,
89 pub worktree_state_digest: String,
90 pub analyzed_content_digest: Option<String>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
94#[serde(deny_unknown_fields)]
95pub struct ScopeIdentity {
96 pub mode: String,
97 pub path: Option<String>,
98 pub selected_path_count: usize,
99 pub selected_path_digest: String,
100}
101
102#[derive(Debug, Clone, Default, Serialize, Deserialize)]
103pub struct SkippedCounts {
104 pub ignored: usize,
105 pub missing: usize,
106 pub binary: usize,
107 pub undecodable: usize,
108}
109
110#[derive(Debug, Clone)]
111pub struct InventoryFile {
112 pub path: String,
113 pub bytes: usize,
114 pub lines: usize,
115 pub blank_lines: usize,
116 pub code_lines: usize,
117 pub comment_lines: usize,
118 pub language: String,
119 pub profile: String,
120 pub classification: String,
121 pub generated_from: Vec<String>,
122 pub generated_provenance: Value,
123 pub content_sha256: String,
125 pub text: String,
126 pub analysis_status: String,
127 pub skipped_reason: Option<String>,
128 pub symlink_metadata: Option<Value>,
129}
130
131#[derive(Debug, Clone, Default, Serialize, Deserialize)]
132pub struct HistoryMetrics {
133 pub first_seen_timestamp: Option<i64>,
134 pub age_days: u64,
135 pub revisions_window: usize,
136 pub recency_weighted_commits: f64,
137 pub added_window: usize,
138 pub deleted_window: usize,
139 pub line_churn_window: usize,
140 pub token_churn_window: usize,
141 pub relative_churn_window: f64,
142 pub late_churn_spike: f64,
143 pub author_count_window: usize,
144 pub author_entropy: f64,
145 pub top_author_share: f64,
146 pub days_since_non_bot_edit: Option<u64>,
147 pub recent_maintainer_diversity: usize,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct CommitRecord {
152 pub commit: String,
153 pub timestamp: i64,
154 pub author: String,
155 pub paths: Vec<String>,
156 pub line_churn_by_path: BTreeMap<String, usize>,
157 pub change_set_size: usize,
158 pub change_kind: String,
159 pub calibration_weight: f64,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163#[serde(deny_unknown_fields)]
164pub struct FileAnalysis {
165 pub path: String,
166 pub bytes: usize,
167 pub lines: usize,
168 pub blank_lines: usize,
169 pub code_lines: usize,
170 pub comment_lines: usize,
171 pub language: String,
172 pub profile: String,
173 pub classification: String,
174 pub generated_from: Vec<String>,
175 pub generated_provenance: Value,
176 pub analysis_status: String,
177 pub skipped_reason: Option<String>,
178 pub symlink_metadata: Option<Value>,
179 pub has_inline_tests: bool,
180 pub tokens: usize,
181 pub context_band: String,
182 pub context_pressure: f64,
183 pub content_fingerprint: String,
186 pub content_sha256: String,
188 #[serde(skip)]
189 pub structural_tokens: Vec<String>,
190 pub structural_token_count: usize,
191 pub top_structural_terms: Vec<String>,
192 pub structural_categories: Value,
193 pub age_days: u64,
194 pub revisions_window: usize,
195 pub recency_weighted_commits: f64,
196 pub added_window: usize,
197 pub deleted_window: usize,
198 pub churn_lines_window: usize,
199 pub line_churn_window: usize,
200 pub token_churn_window: usize,
201 pub relative_churn_window: f64,
202 pub late_churn_spike: f64,
203 pub author_count_window: usize,
204 pub author_entropy: f64,
205 pub top_author_share: f64,
206 pub days_since_non_bot_edit: Option<u64>,
207 pub recent_maintainer_diversity: usize,
208 pub age_pressure: f64,
209 pub revision_norm: f64,
210 pub relative_churn_norm: f64,
211 pub churn_pressure: f64,
212 pub slop_score: f64,
213 pub slop_band: String,
214 pub reason_codes: Vec<String>,
215 pub costs: Value,
216 pub overlays: Value,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
220#[serde(deny_unknown_fields)]
221pub struct FolderAnalysis {
222 pub path: String,
223 pub descendant_file_count: usize,
224 pub direct_file_count: usize,
225 pub bytes: usize,
226 pub lines: usize,
227 pub tokens: usize,
228 pub direct_tokens: usize,
229 pub context_band: String,
230 pub health_band: String,
231 pub context_pressure: f64,
232 pub slop_score: f64,
233 pub slop_band: String,
234 pub reason_codes: Vec<String>,
235 pub top_file_path: String,
236 pub classification: String,
237 pub costs: Value,
238 pub overlays: Value,
239}
240
241#[derive(Debug, Clone, Default, Serialize, Deserialize)]
242pub struct OrganizationAnalysis {
243 pub organization_metrics: Value,
244 pub relationships: Value,
245 pub clusters: Value,
246 pub file_overlays: BTreeMap<String, Value>,
247 pub folder_overlays: BTreeMap<String, Value>,
248 pub top_structural_files: Vec<Value>,
249}
250
251#[derive(Debug, Clone)]
252pub struct Analysis {
253 pub output_root: PathBuf,
254 pub report_profile: String,
255 pub compression: String,
256 pub repo: RepoMetadata,
257 pub config: Value,
258 pub generated_at: String,
259 pub analyzed_revision_at: Option<String>,
260 pub skipped: SkippedCounts,
261 pub tracked_file_count: usize,
262 pub scope: ScopeIdentity,
263 pub files: Vec<FileAnalysis>,
264 pub folders: Vec<FolderAnalysis>,
265 pub organization: OrganizationAnalysis,
266 pub action_queue: Vec<Value>,
267 pub observation_feed: Vec<Value>,
268 pub diagnostics: Value,
269}
270
271#[derive(Debug, Clone)]
272pub struct FindResult {
273 pub report: Value,
274 pub report_json: PathBuf,
275 pub report_yaml: PathBuf,
277 pub summary_md: PathBuf,
278 pub health_md: PathBuf,
279 pub compressed_report: Option<PathBuf>,
280 pub terminal: String,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
284#[serde(deny_unknown_fields)]
285pub struct Finding {
286 pub path: String,
287 pub profile: String,
288 pub severity: String,
289 pub title: String,
290 pub message: String,
291 pub next_command: String,
292 pub slop_band: String,
293 pub context_band: String,
294 pub slop_score: f64,
295 pub tokens: usize,
296 pub reasons: Vec<String>,
297}
298
299#[derive(Debug, Clone, Default, Serialize, Deserialize)]
300#[serde(deny_unknown_fields)]
301pub struct HealthRollup {
302 pub file_band_counts: BTreeMap<String, usize>,
303 pub folder_band_counts: BTreeMap<String, usize>,
304 pub profile_rollups: Vec<Value>,
305 pub language_rollups: BTreeMap<String, Vec<Value>>,
306 pub file_distribution: Value,
307 pub folder_distribution: Value,
308 pub refactor_candidates: Vec<Value>,
309 pub watchlist: Vec<Value>,
310 pub findings: Vec<Finding>,
311}
312
313pub fn parent_folders(path: &str) -> Vec<String> {
314 let normalized = path.trim_matches('/');
315 let mut result = vec![".".to_string()];
316 let parts: Vec<&str> = normalized.split('/').collect();
317 if parts.len() <= 1 {
318 return result;
319 }
320 for index in 1..parts.len() {
321 result.push(parts[..index].join("/"));
322 }
323 result
324}
325
326pub fn top_level_root(path: &str) -> String {
327 path.trim_matches('/')
328 .split('/')
329 .next()
330 .filter(|part| !part.is_empty())
331 .unwrap_or(".")
332 .to_string()
333}