1use crate::constants::FastHashMap;
11use serde::{Deserialize, Serialize, Serializer, ser::SerializeMap};
12
13fn serialize_conversation_usage<S>(
19 usage: &FastHashMap<String, serde_json::Value>,
20 serializer: S,
21) -> Result<S::Ok, S::Error>
22where
23 S: Serializer,
24{
25 let mut entries: Vec<_> = usage.iter().collect();
26 entries.sort_unstable_by_key(|(model, _)| *model);
27
28 let mut map = serializer.serialize_map(Some(entries.len()))?;
29 for (model, value) in entries {
30 map.serialize_entry(model, value)?;
31 }
32 map.end()
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct CodeAnalysisDetailBase {
43 pub file_path: String,
45 pub line_count: usize,
47 pub character_count: usize,
49 pub timestamp: i64,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct CodeAnalysisWriteDetail {
60 #[serde(flatten)]
62 pub base: CodeAnalysisDetailBase,
63 pub content: String,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct CodeAnalysisReadDetail {
74 #[serde(flatten)]
76 pub base: CodeAnalysisDetailBase,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct CodeAnalysisApplyDiffDetail {
86 #[serde(flatten)]
88 pub base: CodeAnalysisDetailBase,
89 pub old_string: String,
91 pub new_string: String,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct CodeAnalysisRunCommandDetail {
102 #[serde(flatten)]
104 pub base: CodeAnalysisDetailBase,
105 pub command: String,
107 pub description: String,
109}
110
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
116#[serde(rename_all = "PascalCase")]
117pub struct CodeAnalysisToolCalls {
118 pub read: usize,
120 pub write: usize,
122 pub edit: usize,
124 pub todo_write: usize,
126 pub bash: usize,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(rename_all = "camelCase")]
138pub struct CodeAnalysisRecord {
139 pub total_unique_files: usize,
141 pub total_write_lines: usize,
143 pub total_read_lines: usize,
145 pub total_edit_lines: usize,
147 pub total_write_characters: usize,
149 pub total_read_characters: usize,
151 pub total_edit_characters: usize,
153 pub write_file_details: Vec<CodeAnalysisWriteDetail>,
155 pub read_file_details: Vec<CodeAnalysisReadDetail>,
157 pub edit_file_details: Vec<CodeAnalysisApplyDiffDetail>,
159 pub run_command_details: Vec<CodeAnalysisRunCommandDetail>,
161 pub tool_call_counts: CodeAnalysisToolCalls,
163 #[serde(serialize_with = "serialize_conversation_usage")]
166 pub conversation_usage: FastHashMap<String, serde_json::Value>,
167 #[serde(skip)]
176 pub advisor_usage: FastHashMap<String, serde_json::Value>,
177 pub task_id: String,
179 pub timestamp: i64,
181 pub folder_path: String,
183 pub git_remote_url: String,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
195#[serde(rename_all = "camelCase")]
196pub struct CodeAnalysis {
197 pub user: String,
199 pub extension_name: String,
202 pub insights_version: String,
204 pub machine_id: String,
206 pub records: Vec<CodeAnalysisRecord>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
223#[serde(rename_all = "camelCase")]
224pub struct AggregatedAnalysisRow {
225 pub model: String,
227 pub edit_lines: usize,
229 pub read_lines: usize,
231 pub write_lines: usize,
233 pub bash_count: usize,
235 pub edit_count: usize,
237 pub read_count: usize,
239 pub todo_write_count: usize,
241 pub write_count: usize,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
260pub enum ExtensionType {
261 ClaudeCode,
263 Codex,
265 Copilot,
267 Gemini,
269 OpenCode,
271 Cursor,
273 Hermes,
275 Grok,
277}
278
279impl ExtensionType {
280 pub fn scan_rank(self) -> u8 {
286 match self {
287 ExtensionType::ClaudeCode => 0,
288 ExtensionType::Codex => 1,
289 ExtensionType::Copilot => 2,
290 ExtensionType::Gemini => 3,
291 ExtensionType::Grok => 4,
292 ExtensionType::OpenCode => 5,
293 ExtensionType::Cursor => 6,
294 ExtensionType::Hermes => 7,
295 }
296 }
297}
298
299impl std::fmt::Display for ExtensionType {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 match self {
302 ExtensionType::ClaudeCode => write!(f, "Claude-Code"),
303 ExtensionType::Codex => write!(f, "Codex"),
304 ExtensionType::Copilot => write!(f, "Copilot-CLI"),
305 ExtensionType::Gemini => write!(f, "Gemini"),
306 ExtensionType::OpenCode => write!(f, "OpenCode"),
307 ExtensionType::Cursor => write!(f, "Cursor"),
308 ExtensionType::Hermes => write!(f, "Hermes"),
309 ExtensionType::Grok => write!(f, "Grok"),
310 }
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn code_analysis_literal_has_no_parser_diagnostic_field() {
320 let analysis = CodeAnalysis {
321 user: String::new(),
322 extension_name: String::new(),
323 insights_version: String::new(),
324 machine_id: String::new(),
325 records: Vec::new(),
326 };
327
328 assert_eq!(
329 serde_json::to_value(analysis).unwrap()["records"],
330 serde_json::json!([])
331 );
332 }
333
334 #[test]
335 fn test_code_analysis_tool_calls_serialization() {
336 let tool_calls = CodeAnalysisToolCalls {
338 read: 10,
339 write: 5,
340 edit: 3,
341 todo_write: 2,
342 bash: 1,
343 };
344
345 let json = serde_json::to_string(&tool_calls).unwrap();
346 let deserialized: CodeAnalysisToolCalls = serde_json::from_str(&json).unwrap();
347
348 assert_eq!(deserialized.read, 10);
349 assert_eq!(deserialized.write, 5);
350 assert_eq!(deserialized.edit, 3);
351 assert_eq!(deserialized.todo_write, 2);
352 assert_eq!(deserialized.bash, 1);
353 }
354
355 #[test]
356 fn test_code_analysis_tool_calls_default() {
357 let tool_calls = CodeAnalysisToolCalls::default();
359
360 assert_eq!(tool_calls.read, 0);
361 assert_eq!(tool_calls.write, 0);
362 assert_eq!(tool_calls.edit, 0);
363 assert_eq!(tool_calls.todo_write, 0);
364 assert_eq!(tool_calls.bash, 0);
365 }
366
367 #[test]
368 fn test_code_analysis_detail_base_serialization() {
369 let detail = CodeAnalysisDetailBase {
371 file_path: "/path/to/file.rs".to_string(),
372 line_count: 100,
373 character_count: 2500,
374 timestamp: 1234567890,
375 };
376
377 let json = serde_json::to_string(&detail).unwrap();
378 let deserialized: CodeAnalysisDetailBase = serde_json::from_str(&json).unwrap();
379
380 assert_eq!(deserialized.file_path, "/path/to/file.rs");
381 assert_eq!(deserialized.line_count, 100);
382 assert_eq!(deserialized.character_count, 2500);
383 assert_eq!(deserialized.timestamp, 1234567890);
384 }
385
386 #[test]
387 fn test_code_analysis_write_detail_serialization() {
388 let write_detail = CodeAnalysisWriteDetail {
390 base: CodeAnalysisDetailBase {
391 file_path: "/test/file.rs".to_string(),
392 line_count: 10,
393 character_count: 250,
394 timestamp: 1234567890,
395 },
396 content: "fn main() {\n println!(\"Hello\");\n}".to_string(),
397 };
398
399 let json = serde_json::to_string(&write_detail).unwrap();
400 let deserialized: CodeAnalysisWriteDetail = serde_json::from_str(&json).unwrap();
401
402 assert_eq!(deserialized.base.file_path, "/test/file.rs");
403 assert_eq!(deserialized.base.line_count, 10);
404 assert!(deserialized.content.contains("main"));
405 }
406
407 #[test]
408 fn test_code_analysis_read_detail_serialization() {
409 let read_detail = CodeAnalysisReadDetail {
411 base: CodeAnalysisDetailBase {
412 file_path: "/test/input.txt".to_string(),
413 line_count: 50,
414 character_count: 1200,
415 timestamp: 1234567890,
416 },
417 };
418
419 let json = serde_json::to_string(&read_detail).unwrap();
420 let deserialized: CodeAnalysisReadDetail = serde_json::from_str(&json).unwrap();
421
422 assert_eq!(deserialized.base.file_path, "/test/input.txt");
423 assert_eq!(deserialized.base.line_count, 50);
424 assert_eq!(deserialized.base.character_count, 1200);
425 }
426
427 #[test]
428 fn test_code_analysis_apply_diff_detail_serialization() {
429 let edit_detail = CodeAnalysisApplyDiffDetail {
431 base: CodeAnalysisDetailBase {
432 file_path: "/test/edit.rs".to_string(),
433 line_count: 5,
434 character_count: 100,
435 timestamp: 1234567890,
436 },
437 old_string: "old content".to_string(),
438 new_string: "new content".to_string(),
439 };
440
441 let json = serde_json::to_string(&edit_detail).unwrap();
442 let deserialized: CodeAnalysisApplyDiffDetail = serde_json::from_str(&json).unwrap();
443
444 assert_eq!(deserialized.base.file_path, "/test/edit.rs");
445 assert_eq!(deserialized.old_string, "old content");
446 assert_eq!(deserialized.new_string, "new content");
447 }
448
449 #[test]
450 fn test_code_analysis_run_command_detail_serialization() {
451 let run_detail = CodeAnalysisRunCommandDetail {
453 base: CodeAnalysisDetailBase {
454 file_path: "/workspace".to_string(),
455 line_count: 0,
456 character_count: 10,
457 timestamp: 1234567890,
458 },
459 command: "cargo test".to_string(),
460 description: "Running tests".to_string(),
461 };
462
463 let json = serde_json::to_string(&run_detail).unwrap();
464 let deserialized: CodeAnalysisRunCommandDetail = serde_json::from_str(&json).unwrap();
465
466 assert_eq!(deserialized.command, "cargo test");
467 assert_eq!(deserialized.description, "Running tests");
468 }
469
470 #[test]
471 fn test_extension_type_equality() {
472 assert_eq!(ExtensionType::ClaudeCode, ExtensionType::ClaudeCode);
474 assert_eq!(ExtensionType::Codex, ExtensionType::Codex);
475 assert_eq!(ExtensionType::Copilot, ExtensionType::Copilot);
476 assert_eq!(ExtensionType::Gemini, ExtensionType::Gemini);
477 assert_eq!(ExtensionType::Grok.to_string(), "Grok");
478
479 assert_ne!(ExtensionType::ClaudeCode, ExtensionType::Codex);
480 assert_ne!(ExtensionType::Copilot, ExtensionType::Gemini);
481 }
482
483 #[test]
484 fn test_extension_type_clone() {
485 let ext1 = ExtensionType::ClaudeCode;
487 let ext2 = ext1;
488
489 assert_eq!(ext1, ext2);
490 }
491
492 #[test]
493 fn test_extension_type_debug() {
494 let ext = ExtensionType::ClaudeCode;
496 let debug_str = format!("{:?}", ext);
497
498 assert!(debug_str.contains("ClaudeCode"));
499 }
500
501 #[test]
502 fn test_code_analysis_tool_calls_clone() {
503 let tool_calls1 = CodeAnalysisToolCalls {
505 read: 5,
506 write: 3,
507 edit: 2,
508 todo_write: 1,
509 bash: 4,
510 };
511
512 let tool_calls2 = tool_calls1.clone();
513
514 assert_eq!(tool_calls1.read, tool_calls2.read);
515 assert_eq!(tool_calls1.write, tool_calls2.write);
516 }
517
518 #[test]
519 fn test_camel_case_serialization() {
520 let detail = CodeAnalysisDetailBase {
522 file_path: "/test".to_string(),
523 line_count: 10,
524 character_count: 100,
525 timestamp: 123,
526 };
527
528 let json = serde_json::to_value(&detail).unwrap();
529
530 assert!(json["filePath"].is_string());
532 assert!(json["lineCount"].is_number());
533 assert!(json["characterCount"].is_number());
534 assert!(json["timestamp"].is_number());
535 }
536
537 #[test]
538 fn test_pascal_case_tool_calls() {
539 let tool_calls = CodeAnalysisToolCalls {
541 read: 1,
542 write: 2,
543 edit: 3,
544 todo_write: 4,
545 bash: 5,
546 };
547
548 let json = serde_json::to_value(&tool_calls).unwrap();
549
550 assert!(json["Read"].is_number());
552 assert!(json["Write"].is_number());
553 assert!(json["Edit"].is_number());
554 }
555
556 #[test]
557 fn test_code_analysis_record_serialization() {
558 let record = CodeAnalysisRecord {
560 total_unique_files: 5,
561 total_write_lines: 100,
562 total_read_lines: 200,
563 total_edit_lines: 50,
564 total_write_characters: 2500,
565 total_read_characters: 5000,
566 total_edit_characters: 1250,
567 write_file_details: vec![],
568 read_file_details: vec![],
569 edit_file_details: vec![],
570 run_command_details: vec![],
571 tool_call_counts: CodeAnalysisToolCalls::default(),
572 conversation_usage: FastHashMap::default(),
573 advisor_usage: FastHashMap::default(),
574 task_id: "task-123".to_string(),
575 timestamp: 1234567890,
576 folder_path: "/workspace".to_string(),
577 git_remote_url: "https://github.com/test/repo".to_string(),
578 };
579
580 let json = serde_json::to_string(&record).unwrap();
581 let deserialized: CodeAnalysisRecord = serde_json::from_str(&json).unwrap();
582
583 assert_eq!(deserialized.total_unique_files, 5);
584 assert_eq!(deserialized.total_write_lines, 100);
585 assert_eq!(deserialized.task_id, "task-123");
586 assert_eq!(deserialized.folder_path, "/workspace");
587 }
588
589 #[test]
590 fn test_empty_details_serialization() {
591 let record = CodeAnalysisRecord {
593 total_unique_files: 0,
594 total_write_lines: 0,
595 total_read_lines: 0,
596 total_edit_lines: 0,
597 total_write_characters: 0,
598 total_read_characters: 0,
599 total_edit_characters: 0,
600 write_file_details: vec![],
601 read_file_details: vec![],
602 edit_file_details: vec![],
603 run_command_details: vec![],
604 tool_call_counts: CodeAnalysisToolCalls::default(),
605 conversation_usage: FastHashMap::default(),
606 advisor_usage: FastHashMap::default(),
607 task_id: String::new(),
608 timestamp: 0,
609 folder_path: String::new(),
610 git_remote_url: String::new(),
611 };
612
613 let json = serde_json::to_string(&record).unwrap();
614 let deserialized: CodeAnalysisRecord = serde_json::from_str(&json).unwrap();
615
616 assert_eq!(deserialized.write_file_details.len(), 0);
617 assert_eq!(deserialized.read_file_details.len(), 0);
618 assert_eq!(deserialized.edit_file_details.len(), 0);
619 assert_eq!(deserialized.run_command_details.len(), 0);
620 }
621}