1use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use std::fs;
15use std::path::PathBuf;
16use std::sync::Mutex;
17use std::time::{SystemTime, UNIX_EPOCH};
18
19const OUTPUT_DIR: &str = "/tmp/syncable-cli/outputs";
21
22const MAX_AGE_SECS: u64 = 3600;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SessionRef {
28 pub ref_id: String,
30 pub tool: String,
32 pub contains: String,
34 pub summary: String,
36 pub timestamp: u64,
38 pub size_bytes: usize,
40}
41
42static SESSION_REGISTRY: Mutex<Vec<SessionRef>> = Mutex::new(Vec::new());
44
45pub fn register_session_ref(
47 ref_id: &str,
48 tool: &str,
49 contains: &str,
50 summary: &str,
51 size_bytes: usize,
52) {
53 if let Ok(mut registry) = SESSION_REGISTRY.lock() {
54 registry.retain(|r| r.ref_id != ref_id);
56
57 registry.push(SessionRef {
58 ref_id: ref_id.to_string(),
59 tool: tool.to_string(),
60 contains: contains.to_string(),
61 summary: summary.to_string(),
62 timestamp: SystemTime::now()
63 .duration_since(UNIX_EPOCH)
64 .map(|d| d.as_secs())
65 .unwrap_or(0),
66 size_bytes,
67 });
68 }
69}
70
71pub fn get_session_refs() -> Vec<SessionRef> {
73 SESSION_REGISTRY
74 .lock()
75 .map(|r| r.clone())
76 .unwrap_or_default()
77}
78
79pub fn cleanup_session_registry() {
81 let now = SystemTime::now()
82 .duration_since(UNIX_EPOCH)
83 .map(|d| d.as_secs())
84 .unwrap_or(0);
85
86 if let Ok(mut registry) = SESSION_REGISTRY.lock() {
87 registry.retain(|r| now - r.timestamp < MAX_AGE_SECS);
88 }
89}
90
91pub fn format_session_refs_for_agent() -> String {
93 let refs = get_session_refs();
94
95 if refs.is_empty() {
96 return String::new();
97 }
98
99 let mut output = String::from("\nš¦ AVAILABLE DATA FOR RETRIEVAL:\n");
100 output.push_str("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
101
102 for r in &refs {
103 let age = SystemTime::now()
104 .duration_since(UNIX_EPOCH)
105 .map(|d| d.as_secs())
106 .unwrap_or(0)
107 .saturating_sub(r.timestamp);
108
109 let age_str = if age < 60 {
110 format!("{}s ago", age)
111 } else {
112 format!("{}m ago", age / 60)
113 };
114
115 output.push_str(&format!(
116 "\n⢠{} [{}]\n Contains: {}\n Summary: {}\n Retrieve: retrieve_output(\"{}\") or with query\n",
117 r.ref_id, age_str, r.contains, r.summary, r.ref_id
118 ));
119 }
120
121 output.push_str("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
122 output.push_str(
123 "Query examples: \"severity:critical\", \"file:deployment.yaml\", \"code:DL3008\"\n",
124 );
125
126 output
127}
128
129fn generate_ref_id() -> String {
131 let timestamp = SystemTime::now()
132 .duration_since(UNIX_EPOCH)
133 .map(|d| d.as_millis())
134 .unwrap_or(0);
135
136 let ts_part = format!("{:x}", timestamp)
138 .chars()
139 .rev()
140 .take(6)
141 .collect::<String>();
142 let rand_part: String = (0..4)
143 .map(|_| {
144 let idx = (timestamp as usize + rand_simple()) % 36;
145 "abcdefghijklmnopqrstuvwxyz0123456789"
146 .chars()
147 .nth(idx)
148 .unwrap()
149 })
150 .collect();
151
152 format!("{}_{}", ts_part, rand_part)
153}
154
155fn rand_simple() -> usize {
157 let ptr = Box::into_raw(Box::new(0u8));
158 let addr = ptr as usize;
159 unsafe { drop(Box::from_raw(ptr)) };
160 addr.wrapping_mul(1103515245).wrapping_add(12345) % (1 << 31)
161}
162
163fn ensure_output_dir() -> std::io::Result<PathBuf> {
165 let path = PathBuf::from(OUTPUT_DIR);
166 if !path.exists() {
167 fs::create_dir_all(&path)?;
168 }
169 Ok(path)
170}
171
172pub fn store_output(output: &Value, tool_name: &str) -> String {
181 let ref_id = format!("{}_{}", tool_name, generate_ref_id());
182
183 if let Ok(dir) = ensure_output_dir() {
184 let path = dir.join(format!("{}.json", ref_id));
185
186 let stored = serde_json::json!({
188 "ref_id": ref_id,
189 "tool": tool_name,
190 "timestamp": SystemTime::now()
191 .duration_since(UNIX_EPOCH)
192 .map(|d| d.as_secs())
193 .unwrap_or(0),
194 "data": output
195 });
196
197 if let Ok(json_str) = serde_json::to_string(&stored) {
198 let _ = fs::write(&path, json_str);
199 }
200 }
201
202 ref_id
203}
204
205pub fn retrieve_output(ref_id: &str) -> Option<Value> {
213 let path = PathBuf::from(OUTPUT_DIR).join(format!("{}.json", ref_id));
214
215 if !path.exists() {
216 return None;
217 }
218
219 let content = fs::read_to_string(&path).ok()?;
220 let stored: Value = serde_json::from_str(&content).ok()?;
221
222 stored.get("data").cloned()
224}
225
226pub fn retrieve_filtered(ref_id: &str, query: Option<&str>) -> Option<Value> {
247 let data = retrieve_output(ref_id)?;
248
249 if is_analyze_project_output(&data) {
251 return retrieve_analyze_project(&data, query);
252 }
253
254 let query = match query {
255 Some(q) if !q.is_empty() => q,
256 _ => return Some(data),
257 };
258
259 let (filter_type, filter_value) = parse_query(query);
261
262 let issues = find_issues_array(&data)?;
264
265 let filtered: Vec<Value> = issues
267 .iter()
268 .filter(|issue| matches_filter(issue, &filter_type, &filter_value))
269 .cloned()
270 .collect();
271
272 Some(serde_json::json!({
273 "query": query,
274 "total_matches": filtered.len(),
275 "results": filtered
276 }))
277}
278
279fn parse_query(query: &str) -> (String, String) {
281 if let Some(idx) = query.find(':') {
282 let (t, v) = query.split_at(idx);
283 (t.to_lowercase(), v[1..].to_string())
284 } else {
285 ("any".to_string(), query.to_string())
287 }
288}
289
290fn find_issues_array(data: &Value) -> Option<Vec<Value>> {
292 let issue_fields = [
293 "issues",
294 "findings",
295 "violations",
296 "warnings",
297 "errors",
298 "recommendations",
299 "results",
300 ];
301
302 for field in &issue_fields {
303 if let Some(arr) = data.get(field).and_then(|v| v.as_array()) {
304 return Some(arr.clone());
305 }
306 }
307
308 if let Some(arr) = data.as_array() {
310 return Some(arr.clone());
311 }
312
313 None
314}
315
316fn matches_filter(issue: &Value, filter_type: &str, filter_value: &str) -> bool {
318 match filter_type {
319 "severity" | "level" => {
320 let sev = issue
321 .get("severity")
322 .or_else(|| issue.get("level"))
323 .and_then(|v| v.as_str())
324 .unwrap_or("");
325 sev.to_lowercase().contains(&filter_value.to_lowercase())
326 }
327 "file" | "path" => {
328 let file = issue
329 .get("file")
330 .or_else(|| issue.get("path"))
331 .or_else(|| issue.get("filename"))
332 .and_then(|v| v.as_str())
333 .unwrap_or("");
334 file.to_lowercase().contains(&filter_value.to_lowercase())
335 }
336 "code" | "rule" => {
337 let code = issue
338 .get("code")
339 .or_else(|| issue.get("rule"))
340 .or_else(|| issue.get("rule_id"))
341 .and_then(|v| v.as_str())
342 .unwrap_or("");
343 code.to_lowercase().contains(&filter_value.to_lowercase())
344 }
345 "container" | "resource" => {
346 let container = issue
347 .get("container")
348 .or_else(|| issue.get("resource"))
349 .or_else(|| issue.get("name"))
350 .and_then(|v| v.as_str())
351 .unwrap_or("");
352 container
353 .to_lowercase()
354 .contains(&filter_value.to_lowercase())
355 }
356 _ => {
357 let issue_str = serde_json::to_string(issue).unwrap_or_default();
359 issue_str
360 .to_lowercase()
361 .contains(&filter_value.to_lowercase())
362 }
363 }
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
372pub enum OutputType {
373 MonorepoAnalysis,
375 ProjectAnalysis,
377 LintResult,
379 OptimizationResult,
381 Generic,
383}
384
385pub fn detect_output_type(data: &Value) -> OutputType {
387 if data.get("projects").is_some() || data.get("is_monorepo").is_some() {
389 return OutputType::MonorepoAnalysis;
390 }
391
392 if data.get("languages").is_some() && data.get("analysis_metadata").is_some() {
394 return OutputType::ProjectAnalysis;
395 }
396
397 if data.get("failures").is_some() {
399 return OutputType::LintResult;
400 }
401
402 if data.get("recommendations").is_some() {
404 return OutputType::OptimizationResult;
405 }
406
407 OutputType::Generic
408}
409
410fn is_analyze_project_output(data: &Value) -> bool {
412 matches!(
413 detect_output_type(data),
414 OutputType::MonorepoAnalysis | OutputType::ProjectAnalysis
415 )
416}
417
418pub fn retrieve_analyze_project(data: &Value, query: Option<&str>) -> Option<Value> {
428 let query = query.unwrap_or("compact:true");
429 let (query_type, query_value) = parse_query(query);
430
431 match query_type.as_str() {
432 "section" => match query_value.as_str() {
433 "summary" => Some(extract_summary(data)),
434 "projects" => Some(extract_projects_list(data)),
435 "frameworks" => Some(extract_all_frameworks(data)),
436 "languages" => Some(extract_all_languages(data)),
437 "services" => Some(extract_all_services(data)),
438 _ => Some(compact_analyze_output(data)),
439 },
440 "project" => extract_project_by_name(data, &query_value),
441 "service" => extract_service_by_name(data, &query_value),
442 "language" => extract_language_details(data, &query_value),
443 "framework" => extract_framework_details(data, &query_value),
444 "compact" => Some(compact_analyze_output(data)),
445 _ => {
446 Some(compact_analyze_output(data))
448 }
449 }
450}
451
452fn extract_summary(data: &Value) -> Value {
454 let mut summary = serde_json::Map::new();
455
456 if let Some(root) = data.get("root_path").and_then(|v| v.as_str()) {
458 summary.insert("root_path".to_string(), Value::String(root.to_string()));
459 }
460 if let Some(mono) = data.get("is_monorepo").and_then(|v| v.as_bool()) {
461 summary.insert("is_monorepo".to_string(), Value::Bool(mono));
462 }
463
464 if let Some(root) = data.get("project_root").and_then(|v| v.as_str()) {
466 summary.insert("project_root".to_string(), Value::String(root.to_string()));
467 }
468 if let Some(arch) = data.get("architecture_type").and_then(|v| v.as_str()) {
469 summary.insert(
470 "architecture_type".to_string(),
471 Value::String(arch.to_string()),
472 );
473 }
474
475 if let Some(projects) = data.get("projects").and_then(|v| v.as_array()) {
477 summary.insert(
478 "project_count".to_string(),
479 Value::Number(projects.len().into()),
480 );
481
482 let names: Vec<Value> = projects
484 .iter()
485 .filter_map(|p| p.get("name").and_then(|n| n.as_str()))
486 .map(|n| Value::String(n.to_string()))
487 .collect();
488 summary.insert("project_names".to_string(), Value::Array(names));
489 }
490
491 if let Some(languages) = data.get("languages").and_then(|v| v.as_array()) {
493 let names: Vec<Value> = languages
494 .iter()
495 .filter_map(|l| l.get("name").and_then(|n| n.as_str()))
496 .map(|n| Value::String(n.to_string()))
497 .collect();
498 summary.insert("languages".to_string(), Value::Array(names));
499 }
500
501 if let Some(techs) = data.get("technologies").and_then(|v| v.as_array()) {
503 let names: Vec<Value> = techs
504 .iter()
505 .filter_map(|t| t.get("name").and_then(|n| n.as_str()))
506 .map(|n| Value::String(n.to_string()))
507 .collect();
508 summary.insert("technologies".to_string(), Value::Array(names));
509 }
510
511 if let Some(services) = data.get("services").and_then(|v| v.as_array()) {
513 summary.insert(
514 "services_count".to_string(),
515 Value::Number(services.len().into()),
516 );
517 let service_names: Vec<Value> = services
519 .iter()
520 .filter_map(|s| s.get("name").and_then(|n| n.as_str()))
521 .map(|n| Value::String(n.to_string()))
522 .collect();
523 if !service_names.is_empty() {
524 summary.insert("services".to_string(), Value::Array(service_names));
525 }
526 }
527
528 Value::Object(summary)
529}
530
531fn extract_projects_list(data: &Value) -> Value {
533 let projects = data.get("projects").and_then(|v| v.as_array());
534
535 let list: Vec<Value> = projects
536 .map(|arr| {
537 arr.iter()
538 .map(|p| {
539 let mut proj = serde_json::Map::new();
540 if let Some(name) = p.get("name") {
541 proj.insert("name".to_string(), name.clone());
542 }
543 if let Some(path) = p.get("path") {
544 proj.insert("path".to_string(), path.clone());
545 }
546 if let Some(cat) = p.get("project_category") {
547 proj.insert("category".to_string(), cat.clone());
548 }
549 if let Some(analysis) = p.get("analysis") {
551 if let Some(langs) = analysis.get("languages").and_then(|v| v.as_array()) {
552 let lang_names: Vec<Value> = langs
553 .iter()
554 .filter_map(|l| l.get("name").and_then(|n| n.as_str()))
555 .map(|n| Value::String(n.to_string()))
556 .collect();
557 proj.insert("languages".to_string(), Value::Array(lang_names));
558 }
559 if let Some(fws) = analysis.get("frameworks").and_then(|v| v.as_array()) {
560 let fw_names: Vec<Value> = fws
561 .iter()
562 .filter_map(|f| f.get("name").and_then(|n| n.as_str()))
563 .map(|n| Value::String(n.to_string()))
564 .collect();
565 proj.insert("frameworks".to_string(), Value::Array(fw_names));
566 }
567 }
568 Value::Object(proj)
569 })
570 .collect()
571 })
572 .unwrap_or_default();
573
574 serde_json::json!({
575 "total_projects": list.len(),
576 "projects": list
577 })
578}
579
580fn extract_project_by_name(data: &Value, name: &str) -> Option<Value> {
582 let projects = data.get("projects").and_then(|v| v.as_array())?;
583
584 let project = projects.iter().find(|p| {
585 p.get("name")
586 .and_then(|n| n.as_str())
587 .map(|n| n.to_lowercase().contains(&name.to_lowercase()))
588 .unwrap_or(false)
589 })?;
590
591 Some(compact_project(project))
592}
593
594fn extract_service_by_name(data: &Value, name: &str) -> Option<Value> {
596 let projects = data.get("projects").and_then(|v| v.as_array())?;
597
598 for project in projects {
599 if let Some(services) = project
600 .get("analysis")
601 .and_then(|a| a.get("services"))
602 .and_then(|s| s.as_array())
603 && let Some(service) = services.iter().find(|s| {
604 s.get("name")
605 .and_then(|n| n.as_str())
606 .map(|n| n.to_lowercase().contains(&name.to_lowercase()))
607 .unwrap_or(false)
608 })
609 {
610 return Some(service.clone());
611 }
612 }
613 None
614}
615
616fn extract_language_details(data: &Value, lang_name: &str) -> Option<Value> {
618 let mut results = Vec::new();
619
620 let process_languages = |languages: &[Value], proj_name: &str, results: &mut Vec<Value>| {
622 for lang in languages {
623 let name = lang.get("name").and_then(|n| n.as_str()).unwrap_or("");
624 if lang_name == "*" || name.to_lowercase().contains(&lang_name.to_lowercase()) {
625 let mut compact_lang = serde_json::Map::new();
626 if !proj_name.is_empty() {
627 compact_lang
628 .insert("project".to_string(), Value::String(proj_name.to_string()));
629 }
630 compact_lang.insert(
631 "name".to_string(),
632 lang.get("name").cloned().unwrap_or(Value::Null),
633 );
634 compact_lang.insert(
635 "version".to_string(),
636 lang.get("version").cloned().unwrap_or(Value::Null),
637 );
638 compact_lang.insert(
639 "confidence".to_string(),
640 lang.get("confidence").cloned().unwrap_or(Value::Null),
641 );
642
643 if let Some(files) = lang.get("files").and_then(|f| f.as_array()) {
645 compact_lang
646 .insert("file_count".to_string(), Value::Number(files.len().into()));
647 }
648
649 results.push(Value::Object(compact_lang));
650 }
651 }
652 };
653
654 if let Some(languages) = data.get("languages").and_then(|v| v.as_array()) {
656 process_languages(languages, "", &mut results);
657 }
658
659 if let Some(projects) = data.get("projects").and_then(|v| v.as_array()) {
661 for project in projects {
662 let proj_name = project
663 .get("name")
664 .and_then(|n| n.as_str())
665 .unwrap_or("unknown");
666
667 if let Some(languages) = project
668 .get("analysis")
669 .and_then(|a| a.get("languages"))
670 .and_then(|l| l.as_array())
671 {
672 process_languages(languages, proj_name, &mut results);
673 }
674 }
675 }
676
677 Some(serde_json::json!({
678 "query": format!("language:{}", lang_name),
679 "total_matches": results.len(),
680 "results": results
681 }))
682}
683
684fn extract_framework_details(data: &Value, fw_name: &str) -> Option<Value> {
686 let mut results = Vec::new();
687
688 let process_techs = |techs: &[Value], proj_name: &str, results: &mut Vec<Value>| {
690 for tech in techs {
691 let name = tech.get("name").and_then(|n| n.as_str()).unwrap_or("");
692 if fw_name == "*" || name.to_lowercase().contains(&fw_name.to_lowercase()) {
693 let mut compact_fw = serde_json::Map::new();
694 if !proj_name.is_empty() {
695 compact_fw.insert("project".to_string(), Value::String(proj_name.to_string()));
696 }
697 if let Some(v) = tech.get("name") {
698 compact_fw.insert("name".to_string(), v.clone());
699 }
700 if let Some(v) = tech.get("version") {
701 compact_fw.insert("version".to_string(), v.clone());
702 }
703 if let Some(v) = tech.get("category") {
704 compact_fw.insert("category".to_string(), v.clone());
705 }
706 results.push(Value::Object(compact_fw));
707 }
708 }
709 };
710
711 if let Some(techs) = data.get("technologies").and_then(|v| v.as_array()) {
713 process_techs(techs, "", &mut results);
714 }
715
716 if let Some(fws) = data.get("frameworks").and_then(|v| v.as_array()) {
718 process_techs(fws, "", &mut results);
719 }
720
721 if let Some(projects) = data.get("projects").and_then(|v| v.as_array()) {
723 for project in projects {
724 let proj_name = project
725 .get("name")
726 .and_then(|n| n.as_str())
727 .unwrap_or("unknown");
728
729 if let Some(frameworks) = project
730 .get("analysis")
731 .and_then(|a| a.get("frameworks"))
732 .and_then(|f| f.as_array())
733 {
734 process_techs(frameworks, proj_name, &mut results);
735 }
736 }
737 }
738
739 Some(serde_json::json!({
740 "query": format!("framework:{}", fw_name),
741 "total_matches": results.len(),
742 "results": results
743 }))
744}
745
746fn extract_all_frameworks(data: &Value) -> Value {
748 extract_framework_details(data, "*").unwrap_or(serde_json::json!({"results": []}))
749}
750
751fn extract_all_languages(data: &Value) -> Value {
753 extract_language_details(data, "*").unwrap_or(serde_json::json!({"results": []}))
754}
755
756fn extract_all_services(data: &Value) -> Value {
759 extract_projects_list(data)
762}
763
764fn compact_analyze_output(data: &Value) -> Value {
766 let mut result = serde_json::Map::new();
767
768 if let Some(v) = data.get("root_path") {
770 result.insert("root_path".to_string(), v.clone());
771 }
772 if let Some(v) = data.get("is_monorepo") {
773 result.insert("is_monorepo".to_string(), v.clone());
774 }
775
776 if let Some(projects) = data.get("projects").and_then(|v| v.as_array()) {
778 let compacted: Vec<Value> = projects.iter().map(compact_project).collect();
779 result.insert("projects".to_string(), Value::Array(compacted));
780 return Value::Object(result);
781 }
782
783 if let Some(v) = data.get("project_root") {
785 result.insert("project_root".to_string(), v.clone());
786 }
787 if let Some(v) = data.get("architecture_type") {
788 result.insert("architecture_type".to_string(), v.clone());
789 }
790 if let Some(v) = data.get("project_type") {
791 result.insert("project_type".to_string(), v.clone());
792 }
793
794 if let Some(languages) = data.get("languages").and_then(|v| v.as_array()) {
796 let compacted: Vec<Value> = languages
797 .iter()
798 .map(|lang| {
799 let mut compact_lang = serde_json::Map::new();
800 for key in &["name", "version", "confidence"] {
801 if let Some(v) = lang.get(*key) {
802 compact_lang.insert(key.to_string(), v.clone());
803 }
804 }
805 if let Some(files) = lang.get("files").and_then(|f| f.as_array()) {
807 compact_lang
808 .insert("file_count".to_string(), Value::Number(files.len().into()));
809 }
810 Value::Object(compact_lang)
811 })
812 .collect();
813 result.insert("languages".to_string(), Value::Array(compacted));
814 }
815
816 if let Some(techs) = data.get("technologies").and_then(|v| v.as_array()) {
818 let compacted: Vec<Value> = techs
819 .iter()
820 .map(|tech| {
821 let mut compact_tech = serde_json::Map::new();
822 for key in &["name", "version", "category", "confidence"] {
823 if let Some(v) = tech.get(*key) {
824 compact_tech.insert(key.to_string(), v.clone());
825 }
826 }
827 Value::Object(compact_tech)
828 })
829 .collect();
830 result.insert("technologies".to_string(), Value::Array(compacted));
831 }
832
833 if let Some(services) = data.get("services").and_then(|v| v.as_array()) {
835 result.insert("services".to_string(), Value::Array(services.clone()));
836 }
837
838 if let Some(meta) = data.get("analysis_metadata") {
840 result.insert("analysis_metadata".to_string(), meta.clone());
841 }
842
843 Value::Object(result)
844}
845
846fn compact_project(project: &Value) -> Value {
848 let mut compact = serde_json::Map::new();
849
850 for key in &["name", "path", "project_category"] {
852 if let Some(v) = project.get(*key) {
853 compact.insert(key.to_string(), v.clone());
854 }
855 }
856
857 if let Some(analysis) = project.get("analysis") {
859 let mut compact_analysis = serde_json::Map::new();
860
861 if let Some(v) = analysis.get("project_root") {
863 compact_analysis.insert("project_root".to_string(), v.clone());
864 }
865
866 if let Some(languages) = analysis.get("languages").and_then(|v| v.as_array()) {
868 let compacted: Vec<Value> = languages
869 .iter()
870 .map(|lang| {
871 let mut compact_lang = serde_json::Map::new();
872 for key in &["name", "version", "confidence"] {
873 if let Some(v) = lang.get(*key) {
874 compact_lang.insert(key.to_string(), v.clone());
875 }
876 }
877 if let Some(files) = lang.get("files").and_then(|f| f.as_array()) {
879 compact_lang
880 .insert("file_count".to_string(), Value::Number(files.len().into()));
881 }
882 Value::Object(compact_lang)
883 })
884 .collect();
885 compact_analysis.insert("languages".to_string(), Value::Array(compacted));
886 }
887
888 for key in &[
890 "frameworks",
891 "databases",
892 "services",
893 "build_tools",
894 "package_managers",
895 ] {
896 if let Some(v) = analysis.get(*key) {
897 compact_analysis.insert(key.to_string(), v.clone());
898 }
899 }
900
901 compact.insert("analysis".to_string(), Value::Object(compact_analysis));
902 }
903
904 Value::Object(compact)
905}
906
907pub fn list_outputs() -> Vec<OutputInfo> {
909 let dir = match ensure_output_dir() {
910 Ok(d) => d,
911 Err(_) => return Vec::new(),
912 };
913
914 let mut outputs = Vec::new();
915
916 if let Ok(entries) = fs::read_dir(&dir) {
917 for entry in entries.flatten() {
918 if let Some(filename) = entry.file_name().to_str()
919 && filename.ends_with(".json")
920 {
921 let ref_id = filename.trim_end_matches(".json").to_string();
922
923 if let Ok(content) = fs::read_to_string(entry.path())
925 && let Ok(stored) = serde_json::from_str::<Value>(&content)
926 {
927 let tool = stored
928 .get("tool")
929 .and_then(|v| v.as_str())
930 .unwrap_or("unknown")
931 .to_string();
932 let timestamp = stored
933 .get("timestamp")
934 .and_then(|v| v.as_u64())
935 .unwrap_or(0);
936 let size = content.len();
937
938 outputs.push(OutputInfo {
939 ref_id,
940 tool,
941 timestamp,
942 size_bytes: size,
943 });
944 }
945 }
946 }
947 }
948
949 outputs.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
951 outputs
952}
953
954#[derive(Debug, Clone)]
956pub struct OutputInfo {
957 pub ref_id: String,
958 pub tool: String,
959 pub timestamp: u64,
960 pub size_bytes: usize,
961}
962
963pub fn cleanup_old_outputs() {
965 let dir = match ensure_output_dir() {
966 Ok(d) => d,
967 Err(_) => return,
968 };
969
970 let now = SystemTime::now()
971 .duration_since(UNIX_EPOCH)
972 .map(|d| d.as_secs())
973 .unwrap_or(0);
974
975 if let Ok(entries) = fs::read_dir(&dir) {
976 for entry in entries.flatten() {
977 if let Ok(content) = fs::read_to_string(entry.path())
978 && let Ok(stored) = serde_json::from_str::<Value>(&content)
979 {
980 let timestamp = stored
981 .get("timestamp")
982 .and_then(|v| v.as_u64())
983 .unwrap_or(0);
984
985 if now - timestamp > MAX_AGE_SECS {
986 let _ = fs::remove_file(entry.path());
987 }
988 }
989 }
990 }
991}
992
993#[cfg(test)]
994mod tests {
995 use super::*;
996
997 #[test]
998 fn test_store_and_retrieve() {
999 let data = serde_json::json!({
1000 "issues": [
1001 { "code": "test1", "severity": "high", "file": "test.yaml" }
1002 ]
1003 });
1004
1005 let ref_id = store_output(&data, "test_tool");
1006 assert!(ref_id.starts_with("test_tool_"));
1007
1008 let retrieved = retrieve_output(&ref_id);
1009 assert!(retrieved.is_some());
1010 assert_eq!(retrieved.unwrap(), data);
1011 }
1012
1013 #[test]
1014 fn test_filtered_retrieval() {
1015 let data = serde_json::json!({
1016 "issues": [
1017 { "code": "DL3008", "severity": "warning", "file": "Dockerfile1" },
1018 { "code": "DL3009", "severity": "info", "file": "Dockerfile2" },
1019 { "code": "DL3008", "severity": "warning", "file": "Dockerfile3" }
1020 ]
1021 });
1022
1023 let ref_id = store_output(&data, "filter_test");
1024
1025 let filtered = retrieve_filtered(&ref_id, Some("code:DL3008"));
1027 assert!(filtered.is_some());
1028 let results = filtered.unwrap();
1029 assert_eq!(results["total_matches"], 2);
1030
1031 let filtered = retrieve_filtered(&ref_id, Some("severity:info"));
1033 assert!(filtered.is_some());
1034 let results = filtered.unwrap();
1035 assert_eq!(results["total_matches"], 1);
1036 }
1037
1038 #[test]
1039 fn test_parse_query() {
1040 assert_eq!(
1041 parse_query("severity:critical"),
1042 ("severity".to_string(), "critical".to_string())
1043 );
1044 assert_eq!(
1045 parse_query("searchterm"),
1046 ("any".to_string(), "searchterm".to_string())
1047 );
1048 }
1049
1050 #[test]
1051 fn test_analyze_project_detection() {
1052 let analyze_data = serde_json::json!({
1053 "root_path": "/test",
1054 "is_monorepo": true,
1055 "projects": []
1056 });
1057 assert!(is_analyze_project_output(&analyze_data));
1058
1059 let lint_data = serde_json::json!({
1060 "issues": [{ "code": "DL3008" }]
1061 });
1062 assert!(!is_analyze_project_output(&lint_data));
1063 }
1064
1065 #[test]
1066 fn test_analyze_project_summary() {
1067 let data = serde_json::json!({
1068 "root_path": "/test/monorepo",
1069 "is_monorepo": true,
1070 "projects": [
1071 { "name": "api-gateway", "path": "services/api" },
1072 { "name": "web-app", "path": "apps/web" }
1073 ]
1074 });
1075
1076 let summary = extract_summary(&data);
1077 assert_eq!(summary["root_path"], "/test/monorepo");
1078 assert_eq!(summary["is_monorepo"], true);
1079 assert_eq!(summary["project_count"], 2);
1080 }
1081
1082 #[test]
1083 fn test_analyze_project_compact() {
1084 let files: Vec<String> = (0..1000).map(|i| format!("/src/file{}.ts", i)).collect();
1086
1087 let data = serde_json::json!({
1088 "root_path": "/test",
1089 "is_monorepo": false,
1090 "projects": [{
1091 "name": "test-project",
1092 "path": "",
1093 "project_category": "Api",
1094 "analysis": {
1095 "project_root": "/test",
1096 "languages": [{
1097 "name": "TypeScript",
1098 "version": "5.0",
1099 "confidence": 0.95,
1100 "files": files
1101 }],
1102 "frameworks": [{
1103 "name": "React",
1104 "version": "18.0"
1105 }]
1106 }
1107 }]
1108 });
1109
1110 let ref_id = store_output(&data, "analyze_project_test");
1111
1112 let result = retrieve_filtered(&ref_id, None);
1114 assert!(result.is_some());
1115
1116 let compacted = result.unwrap();
1117
1118 let project = &compacted["projects"][0];
1120 let lang = &project["analysis"]["languages"][0];
1121 assert_eq!(lang["name"], "TypeScript");
1122 assert_eq!(lang["file_count"], 1000);
1123 assert!(lang.get("files").is_none()); let compacted_str = serde_json::to_string(&compacted).unwrap();
1127 let original_str = serde_json::to_string(&data).unwrap();
1128 assert!(compacted_str.len() < original_str.len() / 10); }
1130
1131 #[test]
1132 fn test_analyze_project_section_queries() {
1133 let data = serde_json::json!({
1134 "root_path": "/test",
1135 "is_monorepo": true,
1136 "projects": [{
1137 "name": "api-service",
1138 "path": "services/api",
1139 "project_category": "Api",
1140 "analysis": {
1141 "languages": [{
1142 "name": "Go",
1143 "version": "1.21",
1144 "confidence": 0.9,
1145 "files": ["/main.go", "/handler.go"]
1146 }],
1147 "frameworks": [{
1148 "name": "Gin",
1149 "version": "1.9",
1150 "category": "Web"
1151 }],
1152 "services": [{
1153 "name": "api-http",
1154 "type": "http",
1155 "port": 8080
1156 }]
1157 }
1158 }]
1159 });
1160
1161 let ref_id = store_output(&data, "analyze_query_test");
1162
1163 let projects = retrieve_filtered(&ref_id, Some("section:projects"));
1165 assert!(projects.is_some());
1166 assert_eq!(projects.as_ref().unwrap()["total_projects"], 1);
1167
1168 let frameworks = retrieve_filtered(&ref_id, Some("section:frameworks"));
1170 assert!(frameworks.is_some());
1171 assert_eq!(frameworks.as_ref().unwrap()["total_matches"], 1);
1172 assert_eq!(frameworks.as_ref().unwrap()["results"][0]["name"], "Gin");
1173
1174 let languages = retrieve_filtered(&ref_id, Some("section:languages"));
1176 assert!(languages.is_some());
1177 assert_eq!(languages.as_ref().unwrap()["total_matches"], 1);
1178 assert_eq!(languages.as_ref().unwrap()["results"][0]["name"], "Go");
1179 assert_eq!(languages.as_ref().unwrap()["results"][0]["file_count"], 2);
1181
1182 let go = retrieve_filtered(&ref_id, Some("language:Go"));
1184 assert!(go.is_some());
1185 assert_eq!(go.as_ref().unwrap()["total_matches"], 1);
1186
1187 let gin = retrieve_filtered(&ref_id, Some("framework:Gin"));
1189 assert!(gin.is_some());
1190 assert_eq!(gin.as_ref().unwrap()["total_matches"], 1);
1191 }
1192}