1use std::collections::HashSet;
7
8use crate::store::record::FileRecord;
9
10pub mod auto_memory;
11pub mod blast_radius;
12pub mod claude_md;
13pub mod clusters;
14pub mod deps;
15pub mod edges;
16pub mod enrich_signals;
17pub mod git;
18pub mod onboarding;
19pub mod parser;
20pub mod propagation;
21pub mod reparse;
22pub mod resolvers;
23pub mod walker;
24
25pub use auto_memory::{auto_memory_dir, import_auto_memory, AutoMemoryImport};
26pub use claude_md::{import_claude_md, ClaudeMdImport};
27pub use deps::{
28 dep_display_name_from_key, dep_record_key, parse_dep_key, parse_dependencies, DepEcosystem,
29 DepEntry, DepSignals, DepVersion, ManifestKind,
30};
31pub use edges::{build_edges, build_edges_with_root, Layer0Edges};
32pub use git::{mine_git_history, GitSignals};
33pub use parser::{hash_and_parse_parallel, parse_file, parse_files_parallel, StaticFileAnalysis};
34pub use walker::{Language, WalkedFile, Walker};
35
36pub(crate) fn public_api_symbols(analysis: &StaticFileAnalysis) -> Vec<String> {
37 let mut seen = HashSet::new();
38 let mut symbols =
39 Vec::with_capacity(analysis.entry_points.len() + analysis.exported_types.len());
40
41 for symbol in analysis
42 .entry_points
43 .iter()
44 .chain(analysis.exported_types.iter())
45 {
46 if seen.insert(symbol.as_str()) {
47 symbols.push(symbol.clone());
48 }
49 }
50
51 symbols
52}
53
54pub fn build_file_record(
60 file: &WalkedFile,
61 analysis: &StaticFileAnalysis,
62 git: Option<&GitSignals>,
63 hotspot_files: Option<&HashSet<String>>,
64 last_modified_session: u64,
65) -> FileRecord {
66 let path = file.rel_path.clone();
67 let (change_frequency, last_author, is_hotspot) = match git {
68 Some(signals) => (
69 signals.change_frequency.get(&path).copied().unwrap_or(0),
70 signals.last_authors.get(&path).cloned(),
71 hotspot_files
72 .map(|hotspots| hotspots.contains(&path))
73 .unwrap_or(false),
74 ),
75 None => (0, None, false),
76 };
77
78 let token_cost_estimate = (file.size_bytes / 4).min(u32::MAX as u64) as u32;
79 let public_api = public_api_symbols(analysis);
80
81 let mut fr = FileRecord::layer0_stub(
82 path,
83 public_api,
84 analysis.imports.iter().map(|i| i.path.clone()).collect(),
85 analysis.todos.clone(),
86 analysis.unsafe_count,
87 analysis.unwrap_count,
88 change_frequency,
89 last_author,
90 is_hotspot,
91 token_cost_estimate,
92 last_modified_session,
93 );
94
95 if let Some(doc) = &analysis.module_doc {
98 fr.purpose = doc.clone();
99 }
100
101 fr.content_hash = analysis.content_hash.clone();
102 fr.line_count = analysis.line_count;
103
104 fr
105}
106
107pub fn build_file_records(
109 files: &[WalkedFile],
110 analyses: &[StaticFileAnalysis],
111 git: Option<&GitSignals>,
112 last_modified_session: u64,
113) -> Vec<FileRecord> {
114 assert_eq!(
115 files.len(),
116 analyses.len(),
117 "build_file_records expects one analysis per walked file"
118 );
119
120 let hotspot_files = git.map(|signals| {
121 signals
122 .hotspot_files
123 .iter()
124 .cloned()
125 .collect::<HashSet<_>>()
126 });
127 let hotspot_files = hotspot_files.as_ref();
128
129 files
130 .iter()
131 .zip(analyses)
132 .map(|(file, analysis)| {
133 build_file_record(file, analysis, git, hotspot_files, last_modified_session)
134 })
135 .collect()
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::analysis::parser::{ImportKind, ImportStatement};
142 use crate::store::record::TodoComment;
143
144 #[test]
145 fn build_file_record_uses_layer0_defaults_and_git_signals() {
146 let analysis = StaticFileAnalysis {
147 path: "src/lib.rs".to_string(),
148 language: Language::Rust,
149 entry_points: vec!["run".to_string()],
150 exported_types: vec![],
151 imports: vec![ImportStatement::new("crate::utils", ImportKind::Normal, 1)],
152 todos: vec![TodoComment {
153 text: "TODO: tighten docs".to_string(),
154 line: 12,
155 kind: crate::store::record::TodoKind::Todo,
156 }],
157 unsafe_count: 1,
158 unwrap_count: 2,
159 panic_count: 0,
160 branch_count: 3,
161 module_doc: None,
162 content_hash: None,
163 line_count: 0,
164 };
165
166 let mut git = GitSignals::empty();
167 git.change_frequency.insert("src/lib.rs".to_string(), 9);
168 git.last_authors
169 .insert("src/lib.rs".to_string(), "ioni".to_string());
170 git.hotspot_files.push("src/lib.rs".to_string());
171
172 let file = WalkedFile {
173 abs_path: std::path::PathBuf::from("/repo/src/lib.rs"),
174 rel_path: "src/lib.rs".to_string(),
175 language: Language::Rust,
176 size_bytes: 400,
177 mtime_secs: 0,
178 };
179
180 let hotspots = git.hotspot_files.iter().cloned().collect::<HashSet<_>>();
181
182 let record = build_file_record(&file, &analysis, Some(&git), Some(&hotspots), 1234);
183
184 assert_eq!(record.path, "src/lib.rs");
185 assert!(record.purpose.is_empty());
186 assert_eq!(record.entry_points, vec!["run".to_string()]);
187 assert_eq!(record.imports, vec!["crate::utils".to_string()]);
188 assert_eq!(record.todos.len(), 1);
189 assert_eq!(record.unsafe_count, 1);
190 assert_eq!(record.unwrap_count, 2);
191 assert_eq!(record.change_frequency, 9);
192 assert_eq!(record.last_author.as_deref(), Some("ioni"));
193 assert!(record.is_hotspot);
194 assert_eq!(record.token_cost_estimate, 100);
195 assert_eq!(record.last_modified_session, 1234);
196 }
197
198 #[test]
199 fn module_doc_propagates_to_purpose() {
200 let analysis = StaticFileAnalysis {
201 path: "src/auth.rs".to_string(),
202 language: Language::Rust,
203 entry_points: vec![],
204 exported_types: vec![],
205 imports: vec![],
206 todos: vec![],
207 unsafe_count: 0,
208 unwrap_count: 0,
209 panic_count: 0,
210 branch_count: 0,
211 module_doc: Some("Handles JWT authentication.".to_string()),
212 content_hash: None,
213 line_count: 0,
214 };
215 let file = WalkedFile {
216 abs_path: std::path::PathBuf::from("/repo/src/auth.rs"),
217 rel_path: "src/auth.rs".to_string(),
218 language: Language::Rust,
219 size_bytes: 100,
220 mtime_secs: 0,
221 };
222 let record = build_file_record(&file, &analysis, None, None, 0);
223 assert_eq!(record.purpose, "Handles JWT authentication.");
224 }
225
226 #[test]
227 fn exported_types_are_folded_into_stored_api_surface() {
228 let analysis = StaticFileAnalysis {
229 path: "src/models.rs".to_string(),
230 language: Language::Rust,
231 entry_points: vec!["build".to_string()],
232 exported_types: vec!["Widget".to_string(), "Widget".to_string()],
233 imports: vec![],
234 todos: vec![],
235 unsafe_count: 0,
236 unwrap_count: 0,
237 panic_count: 0,
238 branch_count: 0,
239 module_doc: None,
240 content_hash: None,
241 line_count: 0,
242 };
243 let file = WalkedFile {
244 abs_path: std::path::PathBuf::from("/repo/src/models.rs"),
245 rel_path: "src/models.rs".to_string(),
246 language: Language::Rust,
247 size_bytes: 100,
248 mtime_secs: 0,
249 };
250
251 let record = build_file_record(&file, &analysis, None, None, 0);
252 assert_eq!(
253 record.entry_points,
254 vec!["build".to_string(), "Widget".to_string()]
255 );
256 }
257
258 #[test]
259 fn build_file_records_is_stable_for_missing_git_signals() {
260 let files = vec![WalkedFile {
261 abs_path: std::path::PathBuf::from("/repo/src/main.rs"),
262 rel_path: "src/main.rs".to_string(),
263 language: Language::Rust,
264 size_bytes: 8,
265 mtime_secs: 0,
266 }];
267 let analyses = vec![StaticFileAnalysis {
268 path: "src/main.rs".to_string(),
269 language: Language::Rust,
270 entry_points: vec![],
271 exported_types: vec![],
272 imports: vec![],
273 todos: vec![],
274 unsafe_count: 0,
275 unwrap_count: 0,
276 panic_count: 0,
277 branch_count: 0,
278 module_doc: None,
279 content_hash: None,
280 line_count: 0,
281 }];
282
283 let records = build_file_records(&files, &analyses, None, 55);
284
285 assert_eq!(records.len(), 1);
286 assert_eq!(records[0].path, "src/main.rs");
287 assert_eq!(records[0].change_frequency, 0);
288 assert!(records[0].last_author.is_none());
289 assert!(!records[0].is_hotspot);
290 assert_eq!(records[0].token_cost_estimate, 2);
291 }
292}