infiniloom_engine/
types.rs

1//! Core type definitions for Infiniloom
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::path::PathBuf;
6
7// Re-export canonical tokenizer types from tokenizer module
8pub use crate::tokenizer::{TokenCounts, TokenModel};
9
10/// Backward-compatible alias for TokenModel
11///
12/// # Important: No Conversion Needed
13///
14/// `TokenizerModel` and `TokenModel` are the **same type** - this is a type alias,
15/// not a separate type. Any function expecting `TokenModel` can directly accept
16/// `TokenizerModel` without conversion.
17///
18/// **Before (incorrect, ~30 lines of duplication)**:
19/// ```ignore
20/// fn to_token_model(model: TokenizerModel) -> TokenModel {
21///     match model {
22///         TokenizerModel::Claude => TokenModel::Claude,
23///         // ... 26 more identical mappings
24///     }
25/// }
26/// ```
27///
28/// **After (correct)**:
29/// ```ignore
30/// // Direct usage - no conversion function needed
31/// let tokenizer = Tokenizer::new();
32/// tokenizer.count(text, model) // Works with TokenizerModel directly
33/// ```
34///
35/// This alias exists solely for backward compatibility with legacy CLI code that
36/// used the name `TokenizerModel`. All new code should prefer `TokenModel`.
37///
38/// Eliminated in Phase 1 refactoring (Item 2): Removed 193 lines of duplicate
39/// conversion functions and tests from pack.rs and diff.rs.
40pub type TokenizerModel = TokenModel;
41
42/// A scanned repository
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Repository {
45    /// Repository name (usually directory name)
46    pub name: String,
47    /// Absolute path to repository root
48    pub path: PathBuf,
49    /// List of files in the repository
50    pub files: Vec<RepoFile>,
51    /// Repository metadata and statistics
52    pub metadata: RepoMetadata,
53}
54
55impl Repository {
56    /// Create a new empty repository
57    pub fn new(name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
58        Self {
59            name: name.into(),
60            path: path.into(),
61            files: Vec::new(),
62            metadata: RepoMetadata::default(),
63        }
64    }
65
66    /// Get total token count for a specific model
67    pub fn total_tokens(&self, model: TokenizerModel) -> u32 {
68        self.files.iter().map(|f| f.token_count.get(model)).sum()
69    }
70
71    /// Get files filtered by language
72    pub fn files_by_language(&self, language: &str) -> Vec<&RepoFile> {
73        self.files
74            .iter()
75            .filter(|f| f.language.as_deref() == Some(language))
76            .collect()
77    }
78
79    /// Get files sorted by importance
80    #[must_use]
81    pub fn files_by_importance(&self) -> Vec<&RepoFile> {
82        let mut files: Vec<_> = self.files.iter().collect();
83        files.sort_by(|a, b| {
84            b.importance
85                .partial_cmp(&a.importance)
86                .unwrap_or(std::cmp::Ordering::Equal)
87        });
88        files
89    }
90}
91
92impl fmt::Display for Repository {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(
95            f,
96            "Repository({}: {} files, {} lines)",
97            self.name, self.metadata.total_files, self.metadata.total_lines
98        )
99    }
100}
101
102/// A single file in the repository
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct RepoFile {
105    /// Absolute path to file
106    pub path: PathBuf,
107    /// Path relative to repository root
108    pub relative_path: String,
109    /// Detected programming language
110    pub language: Option<String>,
111    /// File size in bytes
112    pub size_bytes: u64,
113    /// Token counts for different models
114    pub token_count: TokenCounts,
115    /// Extracted symbols (functions, classes, etc.)
116    pub symbols: Vec<Symbol>,
117    /// Calculated importance score (0.0 - 1.0)
118    pub importance: f32,
119    /// File content (may be None to save memory)
120    pub content: Option<String>,
121}
122
123impl RepoFile {
124    /// Create a new file entry
125    pub fn new(path: impl Into<PathBuf>, relative_path: impl Into<String>) -> Self {
126        Self {
127            path: path.into(),
128            relative_path: relative_path.into(),
129            language: None,
130            size_bytes: 0,
131            token_count: TokenCounts::default(),
132            symbols: Vec::new(),
133            importance: 0.5,
134            content: None,
135        }
136    }
137
138    /// Get file extension
139    pub fn extension(&self) -> Option<&str> {
140        self.path.extension().and_then(|e| e.to_str())
141    }
142
143    /// Get filename without path
144    #[must_use]
145    pub fn filename(&self) -> &str {
146        self.path.file_name().and_then(|n| n.to_str()).unwrap_or("")
147    }
148}
149
150impl fmt::Display for RepoFile {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        write!(
153            f,
154            "{} ({}, {} tokens)",
155            self.relative_path,
156            self.language.as_deref().unwrap_or("unknown"),
157            self.token_count.claude
158        )
159    }
160}
161
162/// Visibility modifier for symbols
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
164pub enum Visibility {
165    #[default]
166    Public,
167    Private,
168    Protected,
169    Internal, // For languages like C# or package-private in Java
170}
171
172impl Visibility {
173    pub fn name(&self) -> &'static str {
174        match self {
175            Self::Public => "public",
176            Self::Private => "private",
177            Self::Protected => "protected",
178            Self::Internal => "internal",
179        }
180    }
181}
182
183/// A code symbol (function, class, variable, etc.)
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct Symbol {
186    /// Symbol name
187    pub name: String,
188    /// Symbol kind
189    pub kind: SymbolKind,
190    /// Function/method signature (if applicable)
191    pub signature: Option<String>,
192    /// Documentation string
193    pub docstring: Option<String>,
194    /// Starting line number (1-indexed)
195    pub start_line: u32,
196    /// Ending line number (1-indexed)
197    pub end_line: u32,
198    /// Number of references to this symbol
199    pub references: u32,
200    /// Calculated importance (0.0 - 1.0)
201    pub importance: f32,
202    /// Parent symbol name (for methods inside classes)
203    pub parent: Option<String>,
204    /// Visibility modifier (public, private, etc.)
205    pub visibility: Visibility,
206    /// Function/method calls made by this symbol (callee names)
207    pub calls: Vec<String>,
208    /// Base class/parent class name (for class inheritance)
209    pub extends: Option<String>,
210    /// Implemented interfaces/protocols/traits
211    pub implements: Vec<String>,
212}
213
214impl Symbol {
215    /// Create a new symbol
216    pub fn new(name: impl Into<String>, kind: SymbolKind) -> Self {
217        Self {
218            name: name.into(),
219            kind,
220            signature: None,
221            docstring: None,
222            start_line: 0,
223            end_line: 0,
224            references: 0,
225            importance: 0.5,
226            parent: None,
227            visibility: Visibility::default(),
228            calls: Vec::new(),
229            extends: None,
230            implements: Vec::new(),
231        }
232    }
233
234    /// Get line count
235    #[must_use]
236    pub fn line_count(&self) -> u32 {
237        if self.end_line >= self.start_line {
238            self.end_line - self.start_line + 1
239        } else {
240            1
241        }
242    }
243}
244
245impl fmt::Display for Symbol {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        write!(
248            f,
249            "{}:{} (lines {}-{})",
250            self.kind.name(),
251            self.name,
252            self.start_line,
253            self.end_line
254        )
255    }
256}
257
258/// Kind of code symbol
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
260pub enum SymbolKind {
261    Function,
262    Method,
263    Class,
264    Interface,
265    Struct,
266    Enum,
267    Constant,
268    Variable,
269    Import,
270    Export,
271    TypeAlias,
272    Module,
273    Trait,
274    Macro,
275}
276
277impl SymbolKind {
278    /// Get human-readable name
279    #[must_use]
280    pub fn name(&self) -> &'static str {
281        match self {
282            Self::Function => "function",
283            Self::Method => "method",
284            Self::Class => "class",
285            Self::Interface => "interface",
286            Self::Struct => "struct",
287            Self::Enum => "enum",
288            Self::Constant => "constant",
289            Self::Variable => "variable",
290            Self::Import => "import",
291            Self::Export => "export",
292            Self::TypeAlias => "type",
293            Self::Module => "module",
294            Self::Trait => "trait",
295            Self::Macro => "macro",
296        }
297    }
298
299    /// Parse from string name (inverse of name())
300    #[must_use]
301    #[allow(clippy::should_implement_trait)]
302    pub fn from_str(s: &str) -> Option<Self> {
303        match s.to_lowercase().as_str() {
304            "function" => Some(Self::Function),
305            "method" => Some(Self::Method),
306            "class" => Some(Self::Class),
307            "interface" => Some(Self::Interface),
308            "struct" => Some(Self::Struct),
309            "enum" => Some(Self::Enum),
310            "constant" => Some(Self::Constant),
311            "variable" => Some(Self::Variable),
312            "import" => Some(Self::Import),
313            "export" => Some(Self::Export),
314            "type" | "typealias" => Some(Self::TypeAlias),
315            "module" => Some(Self::Module),
316            "trait" => Some(Self::Trait),
317            "macro" => Some(Self::Macro),
318            _ => None,
319        }
320    }
321}
322
323impl std::str::FromStr for SymbolKind {
324    type Err = ();
325
326    fn from_str(s: &str) -> Result<Self, Self::Err> {
327        SymbolKind::from_str(s).ok_or(())
328    }
329}
330
331impl fmt::Display for SymbolKind {
332    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333        write!(f, "{}", self.name())
334    }
335}
336
337/// Repository metadata and statistics
338#[derive(Debug, Clone, Default, Serialize, Deserialize)]
339pub struct RepoMetadata {
340    /// Total number of files
341    pub total_files: u32,
342    /// Total lines of code
343    pub total_lines: u64,
344    /// Aggregate token counts
345    pub total_tokens: TokenCounts,
346    /// Language breakdown
347    pub languages: Vec<LanguageStats>,
348    /// Detected framework (e.g., "React", "Django")
349    pub framework: Option<String>,
350    /// Repository description
351    pub description: Option<String>,
352    /// Git branch (if in git repo)
353    pub branch: Option<String>,
354    /// Git commit hash (if in git repo)
355    pub commit: Option<String>,
356    /// Directory structure tree
357    pub directory_structure: Option<String>,
358    /// External dependencies (packages/libraries)
359    pub external_dependencies: Vec<String>,
360    /// Git history (commits and changes) - for structured output
361    pub git_history: Option<GitHistory>,
362}
363
364/// Statistics for a single language
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct LanguageStats {
367    /// Language name
368    pub language: String,
369    /// Number of files
370    pub files: u32,
371    /// Total lines in this language
372    pub lines: u64,
373    /// Percentage of total codebase
374    pub percentage: f32,
375}
376
377/// A git commit entry for structured output
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct GitCommitInfo {
380    /// Full commit hash
381    pub hash: String,
382    /// Short commit hash (7 chars)
383    pub short_hash: String,
384    /// Author name
385    pub author: String,
386    /// Commit date (YYYY-MM-DD)
387    pub date: String,
388    /// Commit message
389    pub message: String,
390}
391
392/// Git history information for structured output
393#[derive(Debug, Clone, Default, Serialize, Deserialize)]
394pub struct GitHistory {
395    /// Recent commits
396    pub commits: Vec<GitCommitInfo>,
397    /// Files with uncommitted changes
398    pub changed_files: Vec<GitChangedFile>,
399}
400
401/// A file with uncommitted changes
402#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct GitChangedFile {
404    /// File path relative to repo root
405    pub path: String,
406    /// Change status (A=Added, M=Modified, D=Deleted, R=Renamed)
407    pub status: String,
408    /// Diff content (optional, only populated when --include-diffs is used)
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub diff_content: Option<String>,
411}
412
413/// Compression level for output
414#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
415pub enum CompressionLevel {
416    /// No compression
417    None,
418    /// Remove empty lines, trim whitespace
419    Minimal,
420    /// Remove comments, normalize whitespace
421    #[default]
422    Balanced,
423    /// Remove docstrings, keep signatures only
424    Aggressive,
425    /// Key symbols only
426    Extreme,
427    /// Focused: key symbols with small surrounding context
428    Focused,
429    /// Semantic compression using code understanding
430    ///
431    /// Uses chunk-based compression that:
432    /// - Splits content at semantic boundaries (paragraphs, functions)
433    /// - Applies budget-ratio-based selection
434    /// - When `embeddings` feature is enabled: clusters similar code and keeps representatives
435    /// - When disabled: uses heuristic-based sampling
436    ///
437    /// This provides intelligent compression that preserves code structure better than
438    /// character-based approaches, though it's not as sophisticated as full neural
439    /// semantic analysis.
440    ///
441    /// Expected reduction: ~60-70% (may vary based on content structure)
442    Semantic,
443}
444
445impl CompressionLevel {
446    /// Expected reduction percentage
447    ///
448    /// Note: These are approximate values. Actual reduction depends on:
449    /// - Content structure (more repetitive = higher reduction)
450    /// - Code density (comments/whitespace ratio)
451    /// - For Semantic: whether `embeddings` feature is enabled
452    pub fn expected_reduction(&self) -> u8 {
453        match self {
454            Self::None => 0,
455            Self::Minimal => 15,
456            Self::Balanced => 35,
457            Self::Aggressive => 60,
458            Self::Extreme => 80,
459            Self::Focused => 75,
460            // Semantic uses chunk-based compression with ~50% budget ratio default
461            // Combined with structure preservation, typically achieves 60-70%
462            Self::Semantic => 65,
463        }
464    }
465
466    /// Get a human-readable description of this compression level
467    pub fn description(&self) -> &'static str {
468        match self {
469            Self::None => "No compression - original content preserved",
470            Self::Minimal => "Remove empty lines, trim whitespace",
471            Self::Balanced => "Remove comments, normalize whitespace",
472            Self::Aggressive => "Remove docstrings, keep signatures only",
473            Self::Extreme => "Key symbols only - minimal context",
474            Self::Focused => "Focused symbols with small surrounding context",
475            Self::Semantic => "Semantic chunking with intelligent sampling",
476        }
477    }
478
479    /// Parse compression level from string
480    ///
481    /// Accepts: "none", "minimal", "balanced", "aggressive", "extreme", "semantic"
482    /// Case-insensitive. Returns `None` for unrecognized values.
483    #[allow(clippy::should_implement_trait)]
484    pub fn from_str(s: &str) -> Option<Self> {
485        match s.to_lowercase().as_str() {
486            "none" => Some(Self::None),
487            "minimal" => Some(Self::Minimal),
488            "balanced" => Some(Self::Balanced),
489            "aggressive" => Some(Self::Aggressive),
490            "extreme" => Some(Self::Extreme),
491            "focused" => Some(Self::Focused),
492            "semantic" => Some(Self::Semantic),
493            _ => None,
494        }
495    }
496
497    /// Get string name of this compression level
498    pub fn name(&self) -> &'static str {
499        match self {
500            Self::None => "none",
501            Self::Minimal => "minimal",
502            Self::Balanced => "balanced",
503            Self::Aggressive => "aggressive",
504            Self::Extreme => "extreme",
505            Self::Focused => "focused",
506            Self::Semantic => "semantic",
507        }
508    }
509
510    /// Get all available compression levels
511    pub fn all() -> &'static [Self] {
512        &[
513            Self::None,
514            Self::Minimal,
515            Self::Balanced,
516            Self::Aggressive,
517            Self::Extreme,
518            Self::Focused,
519            Self::Semantic,
520        ]
521    }
522}
523
524impl std::str::FromStr for CompressionLevel {
525    type Err = ();
526
527    fn from_str(s: &str) -> Result<Self, Self::Err> {
528        CompressionLevel::from_str(s).ok_or(())
529    }
530}
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535
536    #[test]
537    fn test_repository_new() {
538        let repo = Repository::new("test", "/tmp/test");
539        assert_eq!(repo.name, "test");
540        assert!(repo.files.is_empty());
541    }
542
543    #[test]
544    fn test_repository_total_tokens() {
545        let mut repo = Repository::new("test", "/tmp/test");
546        let mut file1 = RepoFile::new("/tmp/test/a.rs", "a.rs");
547        file1.token_count.set(TokenizerModel::Claude, 100);
548        let mut file2 = RepoFile::new("/tmp/test/b.rs", "b.rs");
549        file2.token_count.set(TokenizerModel::Claude, 200);
550        repo.files.push(file1);
551        repo.files.push(file2);
552        assert_eq!(repo.total_tokens(TokenizerModel::Claude), 300);
553    }
554
555    #[test]
556    fn test_repository_files_by_language() {
557        let mut repo = Repository::new("test", "/tmp/test");
558        let mut file1 = RepoFile::new("/tmp/test/a.rs", "a.rs");
559        file1.language = Some("rust".to_string());
560        let mut file2 = RepoFile::new("/tmp/test/b.py", "b.py");
561        file2.language = Some("python".to_string());
562        let mut file3 = RepoFile::new("/tmp/test/c.rs", "c.rs");
563        file3.language = Some("rust".to_string());
564        repo.files.push(file1);
565        repo.files.push(file2);
566        repo.files.push(file3);
567
568        let rust_files = repo.files_by_language("rust");
569        assert_eq!(rust_files.len(), 2);
570        let python_files = repo.files_by_language("python");
571        assert_eq!(python_files.len(), 1);
572        let go_files = repo.files_by_language("go");
573        assert_eq!(go_files.len(), 0);
574    }
575
576    #[test]
577    fn test_repository_files_by_importance() {
578        let mut repo = Repository::new("test", "/tmp/test");
579        let mut file1 = RepoFile::new("/tmp/test/a.rs", "a.rs");
580        file1.importance = 0.3;
581        let mut file2 = RepoFile::new("/tmp/test/b.rs", "b.rs");
582        file2.importance = 0.9;
583        let mut file3 = RepoFile::new("/tmp/test/c.rs", "c.rs");
584        file3.importance = 0.6;
585        repo.files.push(file1);
586        repo.files.push(file2);
587        repo.files.push(file3);
588
589        let sorted = repo.files_by_importance();
590        assert_eq!(sorted[0].relative_path, "b.rs");
591        assert_eq!(sorted[1].relative_path, "c.rs");
592        assert_eq!(sorted[2].relative_path, "a.rs");
593    }
594
595    #[test]
596    fn test_repository_display() {
597        let mut repo = Repository::new("my-project", "/tmp/my-project");
598        repo.metadata.total_files = 42;
599        repo.metadata.total_lines = 1000;
600        let display = format!("{}", repo);
601        assert!(display.contains("my-project"));
602        assert!(display.contains("42 files"));
603        assert!(display.contains("1000 lines"));
604    }
605
606    #[test]
607    fn test_repo_file_new() {
608        let file = RepoFile::new("/tmp/test/src/main.rs", "src/main.rs");
609        assert_eq!(file.relative_path, "src/main.rs");
610        assert!(file.language.is_none());
611        assert_eq!(file.importance, 0.5);
612    }
613
614    #[test]
615    fn test_repo_file_extension() {
616        let file = RepoFile::new("/tmp/test/main.rs", "main.rs");
617        assert_eq!(file.extension(), Some("rs"));
618
619        let file_no_ext = RepoFile::new("/tmp/test/Makefile", "Makefile");
620        assert_eq!(file_no_ext.extension(), None);
621    }
622
623    #[test]
624    fn test_repo_file_filename() {
625        let file = RepoFile::new("/tmp/test/src/main.rs", "src/main.rs");
626        assert_eq!(file.filename(), "main.rs");
627    }
628
629    #[test]
630    fn test_repo_file_display() {
631        let mut file = RepoFile::new("/tmp/test/main.rs", "main.rs");
632        file.language = Some("rust".to_string());
633        file.token_count.claude = 150;
634        let display = format!("{}", file);
635        assert!(display.contains("main.rs"));
636        assert!(display.contains("rust"));
637        assert!(display.contains("150"));
638    }
639
640    #[test]
641    fn test_repo_file_display_unknown_language() {
642        let file = RepoFile::new("/tmp/test/data.xyz", "data.xyz");
643        let display = format!("{}", file);
644        assert!(display.contains("unknown"));
645    }
646
647    #[test]
648    fn test_token_counts() {
649        let mut counts = TokenCounts::default();
650        counts.set(TokenizerModel::Claude, 100);
651        assert_eq!(counts.get(TokenizerModel::Claude), 100);
652    }
653
654    #[test]
655    fn test_symbol_new() {
656        let sym = Symbol::new("my_function", SymbolKind::Function);
657        assert_eq!(sym.name, "my_function");
658        assert_eq!(sym.kind, SymbolKind::Function);
659        assert_eq!(sym.importance, 0.5);
660        assert!(sym.signature.is_none());
661        assert!(sym.calls.is_empty());
662    }
663
664    #[test]
665    fn test_symbol_line_count() {
666        let mut sym = Symbol::new("test", SymbolKind::Function);
667        sym.start_line = 10;
668        sym.end_line = 20;
669        assert_eq!(sym.line_count(), 11);
670    }
671
672    #[test]
673    fn test_symbol_line_count_single_line() {
674        let mut sym = Symbol::new("test", SymbolKind::Variable);
675        sym.start_line = 5;
676        sym.end_line = 5;
677        assert_eq!(sym.line_count(), 1);
678    }
679
680    #[test]
681    fn test_symbol_line_count_inverted() {
682        let mut sym = Symbol::new("test", SymbolKind::Variable);
683        sym.start_line = 20;
684        sym.end_line = 10; // Inverted
685        assert_eq!(sym.line_count(), 1);
686    }
687
688    #[test]
689    fn test_symbol_display() {
690        let mut sym = Symbol::new("calculate", SymbolKind::Function);
691        sym.start_line = 10;
692        sym.end_line = 25;
693        let display = format!("{}", sym);
694        assert!(display.contains("function"));
695        assert!(display.contains("calculate"));
696        assert!(display.contains("10-25"));
697    }
698
699    #[test]
700    fn test_symbol_kind_name() {
701        assert_eq!(SymbolKind::Function.name(), "function");
702        assert_eq!(SymbolKind::Method.name(), "method");
703        assert_eq!(SymbolKind::Class.name(), "class");
704        assert_eq!(SymbolKind::Interface.name(), "interface");
705        assert_eq!(SymbolKind::Struct.name(), "struct");
706        assert_eq!(SymbolKind::Enum.name(), "enum");
707        assert_eq!(SymbolKind::Constant.name(), "constant");
708        assert_eq!(SymbolKind::Variable.name(), "variable");
709        assert_eq!(SymbolKind::Import.name(), "import");
710        assert_eq!(SymbolKind::Export.name(), "export");
711        assert_eq!(SymbolKind::TypeAlias.name(), "type");
712        assert_eq!(SymbolKind::Module.name(), "module");
713        assert_eq!(SymbolKind::Trait.name(), "trait");
714        assert_eq!(SymbolKind::Macro.name(), "macro");
715    }
716
717    #[test]
718    fn test_symbol_kind_from_str() {
719        assert_eq!(SymbolKind::from_str("function"), Some(SymbolKind::Function));
720        assert_eq!(SymbolKind::from_str("method"), Some(SymbolKind::Method));
721        assert_eq!(SymbolKind::from_str("class"), Some(SymbolKind::Class));
722        assert_eq!(SymbolKind::from_str("interface"), Some(SymbolKind::Interface));
723        assert_eq!(SymbolKind::from_str("struct"), Some(SymbolKind::Struct));
724        assert_eq!(SymbolKind::from_str("enum"), Some(SymbolKind::Enum));
725        assert_eq!(SymbolKind::from_str("constant"), Some(SymbolKind::Constant));
726        assert_eq!(SymbolKind::from_str("variable"), Some(SymbolKind::Variable));
727        assert_eq!(SymbolKind::from_str("import"), Some(SymbolKind::Import));
728        assert_eq!(SymbolKind::from_str("export"), Some(SymbolKind::Export));
729        assert_eq!(SymbolKind::from_str("type"), Some(SymbolKind::TypeAlias));
730        assert_eq!(SymbolKind::from_str("typealias"), Some(SymbolKind::TypeAlias));
731        assert_eq!(SymbolKind::from_str("module"), Some(SymbolKind::Module));
732        assert_eq!(SymbolKind::from_str("trait"), Some(SymbolKind::Trait));
733        assert_eq!(SymbolKind::from_str("macro"), Some(SymbolKind::Macro));
734        // Case insensitive
735        assert_eq!(SymbolKind::from_str("FUNCTION"), Some(SymbolKind::Function));
736        assert_eq!(SymbolKind::from_str("Class"), Some(SymbolKind::Class));
737        // Unknown
738        assert_eq!(SymbolKind::from_str("unknown"), None);
739        assert_eq!(SymbolKind::from_str(""), None);
740    }
741
742    #[test]
743    fn test_symbol_kind_std_from_str() {
744        use std::str::FromStr;
745        assert_eq!("function".parse::<SymbolKind>(), Ok(SymbolKind::Function));
746        assert_eq!("class".parse::<SymbolKind>(), Ok(SymbolKind::Class));
747        assert!("invalid".parse::<SymbolKind>().is_err());
748    }
749
750    #[test]
751    fn test_symbol_kind_display() {
752        assert_eq!(format!("{}", SymbolKind::Function), "function");
753        assert_eq!(format!("{}", SymbolKind::Class), "class");
754    }
755
756    #[test]
757    fn test_visibility_name() {
758        assert_eq!(Visibility::Public.name(), "public");
759        assert_eq!(Visibility::Private.name(), "private");
760        assert_eq!(Visibility::Protected.name(), "protected");
761        assert_eq!(Visibility::Internal.name(), "internal");
762    }
763
764    #[test]
765    fn test_visibility_default() {
766        let vis = Visibility::default();
767        assert_eq!(vis, Visibility::Public);
768    }
769
770    #[test]
771    fn test_language_stats() {
772        let stats = LanguageStats {
773            language: "rust".to_string(),
774            files: 10,
775            lines: 5000,
776            percentage: 45.5,
777        };
778        assert_eq!(stats.language, "rust");
779        assert_eq!(stats.files, 10);
780        assert_eq!(stats.lines, 5000);
781        assert!((stats.percentage - 45.5).abs() < f32::EPSILON);
782    }
783
784    #[test]
785    fn test_git_commit_info() {
786        let commit = GitCommitInfo {
787            hash: "abc123def456".to_string(),
788            short_hash: "abc123d".to_string(),
789            author: "Test Author".to_string(),
790            date: "2025-01-01".to_string(),
791            message: "Test commit".to_string(),
792        };
793        assert_eq!(commit.hash, "abc123def456");
794        assert_eq!(commit.short_hash, "abc123d");
795        assert_eq!(commit.author, "Test Author");
796    }
797
798    #[test]
799    fn test_git_changed_file() {
800        let changed = GitChangedFile {
801            path: "src/main.rs".to_string(),
802            status: "M".to_string(),
803            diff_content: Some("+new line".to_string()),
804        };
805        assert_eq!(changed.path, "src/main.rs");
806        assert_eq!(changed.status, "M");
807        assert!(changed.diff_content.is_some());
808    }
809
810    #[test]
811    fn test_git_history_default() {
812        let history = GitHistory::default();
813        assert!(history.commits.is_empty());
814        assert!(history.changed_files.is_empty());
815    }
816
817    #[test]
818    fn test_repo_metadata_default() {
819        let meta = RepoMetadata::default();
820        assert_eq!(meta.total_files, 0);
821        assert_eq!(meta.total_lines, 0);
822        assert!(meta.languages.is_empty());
823        assert!(meta.framework.is_none());
824        assert!(meta.branch.is_none());
825    }
826
827    #[test]
828    fn test_compression_level_from_str() {
829        assert_eq!(CompressionLevel::from_str("none"), Some(CompressionLevel::None));
830        assert_eq!(CompressionLevel::from_str("minimal"), Some(CompressionLevel::Minimal));
831        assert_eq!(CompressionLevel::from_str("balanced"), Some(CompressionLevel::Balanced));
832        assert_eq!(CompressionLevel::from_str("aggressive"), Some(CompressionLevel::Aggressive));
833        assert_eq!(CompressionLevel::from_str("extreme"), Some(CompressionLevel::Extreme));
834        assert_eq!(CompressionLevel::from_str("focused"), Some(CompressionLevel::Focused));
835        assert_eq!(CompressionLevel::from_str("semantic"), Some(CompressionLevel::Semantic));
836
837        // Case insensitive
838        assert_eq!(CompressionLevel::from_str("SEMANTIC"), Some(CompressionLevel::Semantic));
839        assert_eq!(CompressionLevel::from_str("Balanced"), Some(CompressionLevel::Balanced));
840
841        // Unknown
842        assert_eq!(CompressionLevel::from_str("unknown"), None);
843        assert_eq!(CompressionLevel::from_str(""), None);
844    }
845
846    #[test]
847    fn test_compression_level_std_from_str() {
848        use std::str::FromStr;
849        assert_eq!("balanced".parse::<CompressionLevel>(), Ok(CompressionLevel::Balanced));
850        assert!("invalid".parse::<CompressionLevel>().is_err());
851    }
852
853    #[test]
854    fn test_compression_level_name() {
855        assert_eq!(CompressionLevel::None.name(), "none");
856        assert_eq!(CompressionLevel::Semantic.name(), "semantic");
857    }
858
859    #[test]
860    fn test_compression_level_expected_reduction() {
861        assert_eq!(CompressionLevel::None.expected_reduction(), 0);
862        assert_eq!(CompressionLevel::Minimal.expected_reduction(), 15);
863        assert_eq!(CompressionLevel::Balanced.expected_reduction(), 35);
864        assert_eq!(CompressionLevel::Aggressive.expected_reduction(), 60);
865        assert_eq!(CompressionLevel::Extreme.expected_reduction(), 80);
866        assert_eq!(CompressionLevel::Focused.expected_reduction(), 75);
867        assert_eq!(CompressionLevel::Semantic.expected_reduction(), 65);
868    }
869
870    #[test]
871    fn test_compression_level_description() {
872        // All levels should have non-empty descriptions
873        for level in CompressionLevel::all() {
874            assert!(!level.description().is_empty());
875        }
876    }
877
878    #[test]
879    fn test_compression_level_all() {
880        let all = CompressionLevel::all();
881        assert_eq!(all.len(), 7);
882        assert!(all.contains(&CompressionLevel::Semantic));
883    }
884
885    #[test]
886    fn test_compression_level_default() {
887        let level = CompressionLevel::default();
888        assert_eq!(level, CompressionLevel::Balanced);
889    }
890}