1use std::path::PathBuf;
13
14use anyhow::Result;
15use serde::Serialize;
16
17use crate::cache::{Cache, FileEntry, FileNamingConvention};
18use crate::query::Query;
19
20#[derive(Debug, Clone)]
22pub struct ContextOptions {
23 pub cache: PathBuf,
25 pub json: bool,
27 pub verbose: bool,
29}
30
31impl Default for ContextOptions {
32 fn default() -> Self {
33 Self {
34 cache: PathBuf::from(".acp/acp.cache.json"),
35 json: false,
36 verbose: false,
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub enum ContextOperation {
44 Create {
46 directory: String,
48 },
49 Modify {
51 file: String,
53 },
54 Debug {
56 target: String,
58 },
59 Explore {
61 domain: Option<String>,
63 },
64}
65
66#[derive(Debug, Clone, Serialize)]
68pub struct CreateContext {
69 pub directory: String,
71 pub language: Option<String>,
73 pub naming: Option<FileNamingConvention>,
75 pub import_style: Option<ImportStyle>,
77 pub similar_files: Vec<String>,
79 pub recommended_pattern: Option<String>,
81}
82
83#[derive(Debug, Clone, Serialize)]
85pub struct ModifyContext {
86 pub file: String,
88 pub importers: Vec<String>,
90 pub importer_count: usize,
92 pub constraints: Option<FileConstraint>,
94 pub symbols: Vec<String>,
96 pub domain: Option<String>,
98}
99
100#[derive(Debug, Clone, Serialize)]
102pub struct DebugContext {
103 pub target: String,
105 pub related_files: Vec<String>,
107 pub symbols: Vec<SymbolInfo>,
109 pub hotpaths: Vec<String>,
111}
112
113#[derive(Debug, Clone, Serialize)]
115pub struct ExploreContext {
116 pub domain: Option<String>,
118 pub stats: ProjectStats,
120 pub domains: Vec<DomainInfo>,
122 pub key_files: Vec<String>,
124}
125
126#[derive(Debug, Clone, Serialize)]
128pub struct ImportStyle {
129 pub module_system: String,
131 pub path_style: String,
133 pub index_exports: bool,
135}
136
137#[derive(Debug, Clone, Serialize)]
139pub struct FileConstraint {
140 pub level: String,
142 pub reason: Option<String>,
144}
145
146#[derive(Debug, Clone, Serialize)]
148pub struct SymbolInfo {
149 pub name: String,
151 pub symbol_type: String,
153 pub purpose: Option<String>,
155}
156
157#[derive(Debug, Clone, Serialize)]
159pub struct DomainInfo {
160 pub name: String,
162 pub file_count: usize,
164 pub symbol_count: usize,
166}
167
168#[derive(Debug, Clone, Serialize)]
170pub struct ProjectStats {
171 pub files: usize,
173 pub symbols: usize,
175 pub lines: usize,
177 pub primary_language: Option<String>,
179 pub coverage: f64,
181}
182
183pub fn execute_context(options: ContextOptions, operation: ContextOperation) -> Result<()> {
185 let cache = Cache::from_json(&options.cache)?;
186
187 match operation {
188 ContextOperation::Create { directory } => {
189 execute_create_context(&cache, &directory, &options)
190 }
191 ContextOperation::Modify { file } => execute_modify_context(&cache, &file, &options),
192 ContextOperation::Debug { target } => execute_debug_context(&cache, &target, &options),
193 ContextOperation::Explore { domain } => {
194 execute_explore_context(&cache, domain.as_deref(), &options)
195 }
196 }
197}
198
199fn execute_create_context(cache: &Cache, directory: &str, options: &ContextOptions) -> Result<()> {
201 let naming = cache
203 .conventions
204 .file_naming
205 .iter()
206 .find(|n| n.directory == directory)
207 .cloned()
208 .or_else(|| {
209 cache
211 .conventions
212 .file_naming
213 .iter()
214 .filter(|n| directory.starts_with(&n.directory))
215 .max_by_key(|n| n.directory.len())
216 .cloned()
217 });
218
219 let language = detect_directory_language(cache, directory);
221
222 let import_style = cache.conventions.imports.as_ref().map(|i| ImportStyle {
224 module_system: i
225 .module_system
226 .as_ref()
227 .map(|m| format!("{:?}", m).to_lowercase())
228 .unwrap_or_else(|| "esm".to_string()),
229 path_style: i
230 .path_style
231 .as_ref()
232 .map(|p| format!("{:?}", p).to_lowercase())
233 .unwrap_or_else(|| "relative".to_string()),
234 index_exports: i.index_exports,
235 });
236
237 let similar_files: Vec<String> = cache
239 .files
240 .keys()
241 .filter(|p| {
242 let parent = std::path::Path::new(p)
243 .parent()
244 .map(|p| p.to_string_lossy().to_string())
245 .unwrap_or_default();
246 parent == directory
247 })
248 .take(5)
249 .cloned()
250 .collect();
251
252 let recommended_pattern = naming.as_ref().map(|n| n.pattern.clone());
253
254 let context = CreateContext {
255 directory: directory.to_string(),
256 language,
257 naming,
258 import_style,
259 similar_files,
260 recommended_pattern,
261 };
262
263 output_context(&context, options)
264}
265
266fn execute_modify_context(cache: &Cache, file: &str, options: &ContextOptions) -> Result<()> {
268 let file_entry = cache.files.get(file);
269
270 let importers = file_entry
272 .map(|f| f.imported_by.clone())
273 .unwrap_or_default();
274 let importer_count = importers.len();
275
276 let constraints = cache.constraints.as_ref().and_then(|c| {
278 c.by_file.get(file).and_then(|fc| {
279 fc.mutation.as_ref().map(|m| FileConstraint {
280 level: format!("{:?}", m.level).to_lowercase(),
281 reason: m.reason.clone(),
282 })
283 })
284 });
285
286 let symbols = file_entry.map(|f| f.exports.clone()).unwrap_or_default();
288
289 let domain = cache
291 .domains
292 .iter()
293 .find(|(_, d)| d.files.contains(&file.to_string()))
294 .map(|(name, _)| name.clone());
295
296 let context = ModifyContext {
297 file: file.to_string(),
298 importers,
299 importer_count,
300 constraints,
301 symbols,
302 domain,
303 };
304
305 output_context(&context, options)
306}
307
308fn execute_debug_context(cache: &Cache, target: &str, options: &ContextOptions) -> Result<()> {
310 let (file_path, symbols_info) = if cache.files.contains_key(target) {
312 let file = cache.files.get(target).unwrap();
314 let symbols: Vec<SymbolInfo> = file
315 .exports
316 .iter()
317 .filter_map(|name| cache.symbols.get(name))
318 .map(|s| SymbolInfo {
319 name: s.name.clone(),
320 symbol_type: format!("{:?}", s.symbol_type).to_lowercase(),
321 purpose: s.purpose.clone(),
322 })
323 .collect();
324 (target.to_string(), symbols)
325 } else if let Some(symbol) = cache.symbols.get(target) {
326 (
328 symbol.file.clone(),
329 vec![SymbolInfo {
330 name: symbol.name.clone(),
331 symbol_type: format!("{:?}", symbol.symbol_type).to_lowercase(),
332 purpose: symbol.purpose.clone(),
333 }],
334 )
335 } else {
336 return Err(anyhow::anyhow!(
338 "Target not found: {}. Provide a file path or symbol name.",
339 target
340 ));
341 };
342
343 let related_files = cache
345 .files
346 .get(&file_path)
347 .map(|f| f.imports.clone())
348 .unwrap_or_default();
349
350 let q = Query::new(cache);
352 let hotpaths: Vec<String> = q
353 .hotpaths()
354 .filter(|hp| hp.contains(&file_path) || hp.contains(target))
355 .take(3)
356 .map(String::from)
357 .collect();
358
359 let context = DebugContext {
360 target: target.to_string(),
361 related_files,
362 symbols: symbols_info,
363 hotpaths,
364 };
365
366 output_context(&context, options)
367}
368
369fn execute_explore_context(
371 cache: &Cache,
372 domain_filter: Option<&str>,
373 options: &ContextOptions,
374) -> Result<()> {
375 let stats = ProjectStats {
376 files: cache.stats.files,
377 symbols: cache.stats.symbols,
378 lines: cache.stats.lines,
379 primary_language: cache.stats.primary_language.clone(),
380 coverage: cache.stats.annotation_coverage,
381 };
382
383 let domains: Vec<DomainInfo> = cache
385 .domains
386 .iter()
387 .filter(|(name, _)| domain_filter.is_none_or(|f| name.contains(f)))
388 .map(|(name, d)| DomainInfo {
389 name: name.clone(),
390 file_count: d.files.len(),
391 symbol_count: d.symbols.len(),
392 })
393 .collect();
394
395 let mut key_files: Vec<(&String, &FileEntry)> = cache.files.iter().collect();
397 key_files.sort_by(|a, b| b.1.imported_by.len().cmp(&a.1.imported_by.len()));
398 let key_files: Vec<String> = key_files
399 .iter()
400 .take(10)
401 .map(|(k, _)| (*k).clone())
402 .collect();
403
404 let context = ExploreContext {
405 domain: domain_filter.map(String::from),
406 stats,
407 domains,
408 key_files,
409 };
410
411 output_context(&context, options)
412}
413
414fn output_context<T: Serialize + std::fmt::Debug>(
416 context: &T,
417 _options: &ContextOptions,
418) -> Result<()> {
419 println!("{}", serde_json::to_string_pretty(context)?);
421 Ok(())
422}
423
424fn detect_directory_language(cache: &Cache, directory: &str) -> Option<String> {
426 use std::collections::HashMap;
427
428 let mut lang_counts: HashMap<String, usize> = HashMap::new();
429
430 for (path, file) in &cache.files {
431 let parent = std::path::Path::new(path)
432 .parent()
433 .map(|p| p.to_string_lossy().to_string())
434 .unwrap_or_default();
435
436 if parent == directory || parent.starts_with(&format!("{}/", directory)) {
437 let lang = format!("{:?}", file.language).to_lowercase();
438 *lang_counts.entry(lang).or_insert(0) += 1;
439 }
440 }
441
442 lang_counts
443 .into_iter()
444 .max_by_key(|(_, count)| *count)
445 .map(|(lang, _)| lang)
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn test_import_style_default() {
454 let style = ImportStyle {
455 module_system: "esm".to_string(),
456 path_style: "relative".to_string(),
457 index_exports: false,
458 };
459 assert_eq!(style.module_system, "esm");
460 }
461
462 #[test]
463 fn test_context_options_default() {
464 let opts = ContextOptions::default();
465 assert_eq!(opts.cache, PathBuf::from(".acp/acp.cache.json"));
466 assert!(!opts.json);
467 }
468}