Commit f675ecb

mo khan <mo@mokhan.ca>
2025-07-05 06:39:02
initial port to rust
rs
1 parent 78c6a08
benches/performance_benchmarks.rs
@@ -0,0 +1,335 @@
+//! Performance Benchmarks for Spandx
+//! 
+//! These benchmarks measure the performance of critical components
+//! to ensure the system meets performance requirements and to catch regressions.
+
+use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
+use spandx::core::{Dependency, DependencyCollection, License};
+use spandx::parsers::ruby::GemfileLockParser;
+use spandx::parsers::javascript::PackageLockParser;
+use spandx::spdx::{SpdxCatalogue, LicenseExpression};
+use spandx::cache::Cache;
+use spandx::core::license::calculate_similarity;
+use camino::Utf8PathBuf;
+use tempfile::TempDir;
+use std::fs;
+use tokio::runtime::Runtime;
+
+/// Benchmark dependency creation and manipulation
+fn benchmark_dependency_operations(c: &mut Criterion) {
+    let mut group = c.benchmark_group("dependency_operations");
+    
+    // Benchmark dependency creation
+    group.bench_function("create_dependency", |b| {
+        b.iter(|| {
+            let dep = Dependency::new(
+                black_box("test-package"), 
+                black_box("1.0.0")
+            );
+            black_box(dep)
+        })
+    });
+    
+    // Benchmark dependency collection operations
+    let deps: Vec<_> = (0..1000).map(|i| {
+        Dependency::new(&format!("package-{}", i), "1.0.0")
+    }).collect();
+    
+    group.bench_function("add_1000_dependencies", |b| {
+        b.iter(|| {
+            let mut collection = DependencyCollection::new();
+            for dep in &deps {
+                collection.add(black_box(dep.clone()));
+            }
+            black_box(collection)
+        })
+    });
+    
+    group.bench_function("sort_1000_dependencies", |b| {
+        let mut collection = DependencyCollection::new();
+        for dep in &deps {
+            collection.add(dep.clone());
+        }
+        
+        b.iter(|| {
+            let mut coll = collection.clone();
+            coll.sort_by_name();
+            black_box(coll)
+        })
+    });
+    
+    group.finish();
+}
+
+/// Benchmark SPDX license operations
+fn benchmark_spdx_operations(c: &mut Criterion) {
+    let rt = Runtime::new().unwrap();
+    let mut group = c.benchmark_group("spdx_operations");
+    
+    // Get catalogue once for reuse
+    let catalogue = rt.block_on(async {
+        SpdxCatalogue::fetch().await.unwrap()
+    });
+    
+    group.bench_function("parse_simple_expression", |b| {
+        b.iter(|| {
+            let expr = LicenseExpression::parse(black_box("MIT"));
+            black_box(expr)
+        })
+    });
+    
+    group.bench_function("parse_complex_expression", |b| {
+        b.iter(|| {
+            let expr = LicenseExpression::parse(
+                black_box("(MIT OR Apache-2.0) AND BSD-3-Clause")
+            );
+            black_box(expr)
+        })
+    });
+    
+    group.bench_function("lookup_license", |b| {
+        b.iter(|| {
+            let license = catalogue.get_license(black_box("MIT"));
+            black_box(license)
+        })
+    });
+    
+    group.bench_function("find_similar_licenses", |b| {
+        b.iter(|| {
+            let similar = catalogue.find_similar_licenses(
+                black_box("MIT License\n\nPermission is hereby granted..."), 
+                black_box(0.8)
+            );
+            black_box(similar)
+        })
+    });
+    
+    group.finish();
+}
+
+/// Benchmark license similarity calculations
+fn benchmark_license_similarity(c: &mut Criterion) {
+    let mut group = c.benchmark_group("license_similarity");
+    
+    let mit_text = include_str!("../test_data/licenses/mit.txt");
+    let apache_text = include_str!("../test_data/licenses/apache-2.0.txt");
+    let gpl_text = include_str!("../test_data/licenses/gpl-3.0.txt");
+    
+    // Different text sizes
+    let texts = [
+        ("short", "MIT"),
+        ("medium", &mit_text[..200]),
+        ("long", mit_text),
+        ("very_long", apache_text),
+    ];
+    
+    for (size_name, text1) in &texts {
+        for (_, text2) in &texts {
+            group.bench_with_input(
+                BenchmarkId::new("similarity", format!("{}_{}", size_name, size_name)),
+                &(text1, text2),
+                |b, (t1, t2)| {
+                    b.iter(|| {
+                        let sim = calculate_similarity(black_box(t1), black_box(t2));
+                        black_box(sim)
+                    })
+                }
+            );
+        }
+    }
+    
+    group.finish();
+}
+
+/// Benchmark file parsing operations
+fn benchmark_file_parsing(c: &mut Criterion) {
+    let rt = Runtime::new().unwrap();
+    let mut group = c.benchmark_group("file_parsing");
+    
+    // Create test files
+    let temp_dir = TempDir::new().unwrap();
+    
+    // Small Gemfile.lock
+    let small_gemfile = temp_dir.path().join("small_Gemfile.lock");
+    fs::write(&small_gemfile, create_gemfile_content(10)).unwrap();
+    
+    // Large Gemfile.lock
+    let large_gemfile = temp_dir.path().join("large_Gemfile.lock");
+    fs::write(&large_gemfile, create_gemfile_content(100)).unwrap();
+    
+    // Small package-lock.json
+    let small_package_lock = temp_dir.path().join("small_package-lock.json");
+    fs::write(&small_package_lock, create_package_lock_content(10)).unwrap();
+    
+    // Large package-lock.json
+    let large_package_lock = temp_dir.path().join("large_package-lock.json");
+    fs::write(&large_package_lock, create_package_lock_content(100)).unwrap();
+    
+    let ruby_parser = GemfileLockParser::new();
+    let js_parser = PackageLockParser::new();
+    
+    group.bench_function("parse_small_gemfile", |b| {
+        let path = Utf8PathBuf::try_from(small_gemfile.clone()).unwrap();
+        b.to_async(&rt).iter(|| async {
+            let deps = ruby_parser.parse_file(black_box(&path)).await;
+            black_box(deps)
+        })
+    });
+    
+    group.bench_function("parse_large_gemfile", |b| {
+        let path = Utf8PathBuf::try_from(large_gemfile.clone()).unwrap();
+        b.to_async(&rt).iter(|| async {
+            let deps = ruby_parser.parse_file(black_box(&path)).await;
+            black_box(deps)
+        })
+    });
+    
+    group.bench_function("parse_small_package_lock", |b| {
+        let path = Utf8PathBuf::try_from(small_package_lock.clone()).unwrap();
+        b.to_async(&rt).iter(|| async {
+            let deps = js_parser.parse_file(black_box(&path)).await;
+            black_box(deps)
+        })
+    });
+    
+    group.bench_function("parse_large_package_lock", |b| {
+        let path = Utf8PathBuf::try_from(large_package_lock.clone()).unwrap();
+        b.to_async(&rt).iter(|| async {
+            let deps = js_parser.parse_file(black_box(&path)).await;
+            black_box(deps)
+        })
+    });
+    
+    group.finish();
+}
+
+/// Benchmark cache operations
+fn benchmark_cache_operations(c: &mut Criterion) {
+    let rt = Runtime::new().unwrap();
+    let mut group = c.benchmark_group("cache_operations");
+    
+    let temp_dir = TempDir::new().unwrap();
+    let cache_dir = Utf8PathBuf::try_from(temp_dir.path().to_path_buf()).unwrap();
+    
+    let cache = rt.block_on(async {
+        Cache::new(cache_dir).await.unwrap()
+    });
+    
+    // Test data of different sizes
+    let small_data: Vec<String> = (0..10).map(|i| format!("item-{}", i)).collect();
+    let medium_data: Vec<String> = (0..100).map(|i| format!("item-{}", i)).collect();
+    let large_data: Vec<String> = (0..1000).map(|i| format!("item-{}", i)).collect();
+    
+    group.bench_function("write_small_data", |b| {
+        let mut cache = cache.clone();
+        b.to_async(&rt).iter(|| async {
+            let result = cache.write(black_box("small_key"), black_box(&small_data)).await;
+            black_box(result)
+        })
+    });
+    
+    group.bench_function("write_medium_data", |b| {
+        let mut cache = cache.clone();
+        b.to_async(&rt).iter(|| async {
+            let result = cache.write(black_box("medium_key"), black_box(&medium_data)).await;
+            black_box(result)
+        })
+    });
+    
+    group.bench_function("write_large_data", |b| {
+        let mut cache = cache.clone();
+        b.to_async(&rt).iter(|| async {
+            let result = cache.write(black_box("large_key"), black_box(&large_data)).await;
+            black_box(result)
+        })
+    });
+    
+    // Pre-populate cache for read benchmarks
+    rt.block_on(async {
+        let mut cache = cache.clone();
+        cache.write("read_small", &small_data).await.unwrap();
+        cache.write("read_medium", &medium_data).await.unwrap();
+        cache.write("read_large", &large_data).await.unwrap();
+    });
+    
+    group.bench_function("read_small_data", |b| {
+        let cache = cache.clone();
+        b.to_async(&rt).iter(|| async {
+            let result = cache.read::<Vec<String>>(black_box("read_small")).await;
+            black_box(result)
+        })
+    });
+    
+    group.bench_function("read_medium_data", |b| {
+        let cache = cache.clone();
+        b.to_async(&rt).iter(|| async {
+            let result = cache.read::<Vec<String>>(black_box("read_medium")).await;
+            black_box(result)
+        })
+    });
+    
+    group.bench_function("read_large_data", |b| {
+        let cache = cache.clone();
+        b.to_async(&rt).iter(|| async {
+            let result = cache.read::<Vec<String>>(black_box("read_large")).await;
+            black_box(result)
+        })
+    });
+    
+    group.finish();
+}
+
+/// Helper function to create Gemfile.lock content with specified number of gems
+fn create_gemfile_content(num_gems: usize) -> String {
+    let mut content = String::from("GEM\n  remote: https://rubygems.org/\n  specs:\n");
+    
+    for i in 0..num_gems {
+        content.push_str(&format!("    gem-{} (1.{}.0)\n", i, i));
+    }
+    
+    content.push_str("\nPLATFORMS\n  ruby\n\nDEPENDENCIES\n");
+    
+    for i in 0..num_gems {
+        content.push_str(&format!("  gem-{}\n", i));
+    }
+    
+    content.push_str("\nBUNDLED WITH\n   2.3.7\n");
+    content
+}
+
+/// Helper function to create package-lock.json content with specified number of packages
+fn create_package_lock_content(num_packages: usize) -> String {
+    let mut content = String::from(r#"{"name": "test", "version": "1.0.0", "lockfileVersion": 2, "packages": {"#);
+    
+    content.push_str(r#""": {"version": "1.0.0"},"#);
+    
+    for i in 0..num_packages {
+        content.push_str(&format!(r#""node_modules/package-{}": {{"version": "1.{}.0"}},"#, i, i));
+    }
+    
+    // Remove trailing comma
+    content.pop();
+    
+    content.push_str(r#"}, "dependencies": {"#);
+    
+    for i in 0..num_packages {
+        content.push_str(&format!(r#""package-{}": {{"version": "1.{}.0"}},"#, i, i));
+    }
+    
+    // Remove trailing comma
+    content.pop();
+    
+    content.push_str("}}");
+    content
+}
+
+criterion_group!(
+    benches,
+    benchmark_dependency_operations,
+    benchmark_spdx_operations,
+    benchmark_license_similarity,
+    benchmark_file_parsing,
+    benchmark_cache_operations
+);
+
+criterion_main!(benches);
\ No newline at end of file
examples/error_handling_demo.rs
@@ -0,0 +1,241 @@
+//! Enhanced Error Handling System Demo
+//! 
+//! This example demonstrates the comprehensive error handling system:
+//! - Structured error types with categories
+//! - User-friendly error messages
+//! - Retry logic for retriable errors
+//! - Proper error context and debugging info
+
+use spandx::error::{SpandxError, SpandxResult, ErrorCategory};
+use std::collections::HashMap;
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    println!("๐Ÿšจ Enhanced Error Handling System Demo");
+    println!("=====================================");
+    
+    // Demonstrate different error categories and user messages
+    let errors = vec![
+        // File system errors
+        SpandxError::FileNotFound { 
+            path: "/nonexistent/Gemfile.lock".to_string() 
+        },
+        SpandxError::PermissionDenied { 
+            path: "/etc/shadow".to_string() 
+        },
+        
+        // Network errors
+        SpandxError::NetworkError {
+            method: "GET".to_string(),
+            url: "https://api.github.com/nonexistent".to_string(),
+            source: reqwest::Error::from(reqwest::Client::new().get("http://invalid").send().await.err().unwrap()),
+        },
+        SpandxError::RequestTimeout {
+            url: "https://slow-api.example.com".to_string(),
+            timeout_ms: 30000,
+        },
+        
+        // Package management errors
+        SpandxError::PackageNotFound {
+            package: "nonexistent-package".to_string(),
+            version: "1.0.0".to_string(),
+            registry: "npm".to_string(),
+        },
+        SpandxError::LicenseDetectionError {
+            package: "some-package".to_string(),
+            version: "2.0.0".to_string(),
+            reason: "No license information found in package metadata".to_string(),
+        },
+        
+        // Configuration errors
+        SpandxError::ConfigError {
+            message: "Invalid SPDX cache directory".to_string(),
+            source: None,
+        },
+        SpandxError::InvalidConfigValue {
+            key: "cache.max_size".to_string(),
+            value: "not-a-number".to_string(),
+        },
+        
+        // Parse errors
+        SpandxError::ParseError {
+            file_type: "package-lock.json".to_string(),
+            file_path: "/path/to/package-lock.json".to_string(),
+            source: Box::new(serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidData, "test"))),
+        },
+        SpandxError::InvalidLicenseExpression {
+            expression: "MIT AND AND Apache-2.0".to_string(),
+            source: None,
+        },
+        
+        // Git errors
+        SpandxError::GitError {
+            operation: "clone".to_string(),
+            repository: "https://github.com/nonexistent/repo.git".to_string(),
+            source: git2::Error::from_str("repository not found"),
+        },
+        
+        // Cache errors
+        SpandxError::CacheCorruption {
+            details: "Binary index file has invalid magic number".to_string(),
+        },
+        SpandxError::CacheCapacityError {
+            current_size: 10000,
+            max_size: 5000,
+        },
+        
+        // CLI errors
+        SpandxError::InvalidArguments {
+            message: "Cannot specify both --airgap and --pull flags".to_string(),
+        },
+        SpandxError::NotImplemented {
+            feature: "Docker container scanning".to_string(),
+        },
+    ];
+    
+    // Demonstrate error categorization and user messages
+    println!("\n๐Ÿ“‹ Error Categories and User Messages:");
+    println!("-------------------------------------");
+    
+    let mut category_counts: HashMap<ErrorCategory, usize> = HashMap::new();
+    
+    for (i, error) in errors.iter().enumerate() {
+        let category = error.category();
+        *category_counts.entry(category).or_insert(0) += 1;
+        
+        println!("\n{}. Error Category: {} | Retriable: {}", 
+            i + 1, 
+            category, 
+            if error.is_retriable() { "โœ“" } else { "โœ—" }
+        );
+        println!("   User Message: {}", error.user_message());
+        
+        if error.is_retriable() {
+            if let Some(delay_ms) = error.retry_delay_ms() {
+                println!("   Suggested retry delay: {}ms", delay_ms);
+            }
+        }
+    }
+    
+    // Show category statistics
+    println!("\n๐Ÿ“Š Error Category Statistics:");
+    println!("----------------------------");
+    for (category, count) in &category_counts {
+        println!("  {}: {} errors", category, count);
+    }
+    
+    // Demonstrate error context and chaining
+    println!("\n๐Ÿ”— Error Context and Chaining:");
+    println!("------------------------------");
+    
+    let chained_error = demonstrate_error_chain().await;
+    match chained_error {
+        Err(e) => {
+            println!("Main error: {}", e.user_message());
+            println!("Full error: {:?}", e);
+            println!("Category: {}", e.category());
+        }
+        Ok(_) => println!("No error occurred"),
+    }
+    
+    // Demonstrate retry logic
+    println!("\n๐Ÿ”„ Retry Logic Demonstration:");
+    println!("-----------------------------");
+    
+    let mut attempt = 1;
+    let max_attempts = 3;
+    
+    loop {
+        println!("Attempt {}/{}", attempt, max_attempts);
+        
+        match simulate_network_operation(attempt).await {
+            Ok(result) => {
+                println!("โœ… Success: {}", result);
+                break;
+            }
+            Err(e) => {
+                println!("โŒ Error: {}", e.user_message());
+                
+                if e.is_retriable() && attempt < max_attempts {
+                    if let Some(delay_ms) = e.retry_delay_ms() {
+                        println!("   Retrying in {}ms...", delay_ms);
+                        tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
+                    }
+                    attempt += 1;
+                } else {
+                    println!("   Maximum attempts reached or error not retriable");
+                    break;
+                }
+            }
+        }
+    }
+    
+    // Demonstrate error conversion and convenience functions
+    println!("\n๐Ÿ› ๏ธ  Error Conversion Examples:");
+    println!("-----------------------------");
+    
+    // From standard library errors
+    let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
+    let spandx_error: SpandxError = io_error.into();
+    println!("IO Error โ†’ SpandxError: {}", spandx_error.user_message());
+    
+    // Using convenience constructors
+    let validation_error = SpandxError::validation("version", "must be valid semver");
+    println!("Validation Error: {}", validation_error.user_message());
+    
+    let license_error = SpandxError::license_detection("react", "18.0.0", "SPDX expression parsing failed");
+    println!("License Error: {}", license_error.user_message());
+    
+    println!("\nโœจ Error handling system provides:");
+    println!("  โ€ข Structured error types with rich context");
+    println!("  โ€ข User-friendly error messages");
+    println!("  โ€ข Categorization for metrics and handling");
+    println!("  โ€ข Retry logic for transient failures");
+    println!("  โ€ข Proper error chaining and debugging info");
+    println!("  โ€ข Consistent error handling across all modules");
+    
+    Ok(())
+}
+
+/// Simulate a complex operation that can fail with chained errors
+async fn demonstrate_error_chain() -> SpandxResult<String> {
+    // Simulate parsing a file that leads to a license detection error
+    parse_package_file().await
+        .map_err(|e| SpandxError::DependencyParseError {
+            message: "Failed to extract dependencies".to_string(),
+            source: Some(Box::new(e)),
+        })?;
+    
+    Ok("Successfully processed package file".to_string())
+}
+
+async fn parse_package_file() -> SpandxResult<Vec<String>> {
+    // Simulate a file parsing error
+    Err(SpandxError::ParseError {
+        file_type: "package.json".to_string(),
+        file_path: "/app/package.json".to_string(),
+        source: Box::new(std::io::Error::new(
+            std::io::ErrorKind::InvalidData,
+            "Invalid JSON syntax at line 15"
+        )),
+    })
+}
+
+/// Simulate a network operation that succeeds after retries
+async fn simulate_network_operation(attempt: u32) -> SpandxResult<String> {
+    match attempt {
+        1 => Err(SpandxError::RequestTimeout {
+            url: "https://api.example.com/packages".to_string(),
+            timeout_ms: 5000,
+        }),
+        2 => Err(SpandxError::HttpError {
+            status: 503,
+            url: "https://api.example.com/packages".to_string(),
+            message: "Service temporarily unavailable".to_string(),
+        }),
+        3 => Ok("Successfully fetched package data".to_string()),
+        _ => Err(SpandxError::InternalError {
+            message: "Unexpected attempt number".to_string(),
+        }),
+    }
+}
\ No newline at end of file
examples/hierarchical_cache_demo.rs
@@ -0,0 +1,137 @@
+//! Hierarchical Binary-Indexed Cache System Demo
+//! 
+//! This example demonstrates the multi-level cache hierarchy:
+//! - L1: Fast in-memory LRU cache (configurable size)
+//! - L2: Binary-indexed disk cache with 256 SHA1-based buckets
+//! - L3: Remote package registry fallback (simulated)
+
+use std::time::Instant;
+use spandx::cache::Cache;
+use camino::Utf8PathBuf;
+use tempfile::TempDir;
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    println!("๐Ÿ”— Hierarchical Binary-Indexed Cache System Demo");
+    println!("================================================");
+    
+    // Initialize cache with small L1 cache for demonstration
+    let temp_dir = TempDir::new()?;
+    let cache_dir = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf())
+        .map_err(|e| format!("Failed to convert path: {:?}", e))?;
+    let mut cache = Cache::with_memory_cache_size(cache_dir, 3); // Only 3 entries in L1
+    
+    println!("๐Ÿ“Š Initial cache state:");
+    print_cache_stats(&cache);
+    
+    // Simulate populating cache with package license data
+    let packages = [
+        ("rails", "7.0.0", "rubygems", vec!["MIT".to_string()]),
+        ("express", "4.18.0", "npm", vec!["MIT".to_string()]),
+        ("django", "4.2.0", "pypi", vec!["BSD-3-Clause".to_string()]),
+        ("spring-boot", "3.0.0", "maven", vec!["Apache-2.0".to_string()]),
+        ("react", "18.0.0", "npm", vec!["MIT".to_string()]),
+        ("numpy", "1.24.0", "pypi", vec!["BSD-3-Clause".to_string()]),
+    ];
+    
+    println!("\n๐Ÿ”„ Populating cache with {} packages...", packages.len());
+    for (name, version, pm, licenses) in &packages {
+        cache.set_licenses(name, version, pm, licenses.clone()).await?;
+        println!("  โœ… Cached {}@{} ({}): {:?}", name, version, pm, licenses);
+    }
+    
+    println!("\n๐Ÿ“Š Cache state after population:");
+    print_cache_stats(&cache);
+    
+    // Demonstrate L1 cache hits (fastest)
+    println!("\nโšก Testing L1 cache hits (should be very fast):");
+    for (name, version, pm, expected) in packages.iter().take(3) {
+        let start = Instant::now();
+        let result = cache.get_licenses(name, version, pm).await?;
+        let duration = start.elapsed();
+        
+        match result {
+            Some(licenses) => {
+                println!("  ๐ŸŽฏ L1 HIT: {}@{} -> {:?} ({:.2}ฮผs)", 
+                    name, version, licenses, duration.as_micros());
+                assert_eq!(licenses, *expected);
+            }
+            None => println!("  โŒ MISS: {}@{}", name, version),
+        }
+    }
+    
+    // Clear L1 cache to test L2 fallback
+    println!("\n๐Ÿงน Clearing L1 cache to demonstrate L2 fallback...");
+    cache.clear_memory_cache();
+    print_cache_stats(&cache);
+    
+    // Demonstrate L2 cache hits (slower but still fast)
+    println!("\n๐Ÿ’พ Testing L2 cache hits (binary-indexed disk):");
+    for (name, version, pm, expected) in &packages {
+        let start = Instant::now();
+        let result = cache.get_licenses(name, version, pm).await?;
+        let duration = start.elapsed();
+        
+        match result {
+            Some(licenses) => {
+                println!("  ๐ŸŽฏ L2 HIT: {}@{} -> {:?} ({:.2}ฮผs)", 
+                    name, version, licenses, duration.as_micros());
+                assert_eq!(licenses, *expected);
+            }
+            None => println!("  โŒ MISS: {}@{}", name, version),
+        }
+    }
+    
+    println!("\n๐Ÿ“Š Final cache state (L2 entries promoted to L1):");
+    print_cache_stats(&cache);
+    
+    // Demonstrate cache miss (would trigger L3 fallback in real system)
+    println!("\n๐Ÿ” Testing cache miss (would trigger remote registry lookup):");
+    let start = Instant::now();
+    let result = cache.get_licenses("nonexistent", "1.0.0", "npm").await?;
+    let duration = start.elapsed();
+    
+    match result {
+        Some(licenses) => println!("  ๐ŸŽฏ Unexpected hit: {:?}", licenses),
+        None => println!("  โŒ MISS: nonexistent@1.0.0 -> would fetch from registry ({:.2}ฮผs)", 
+            duration.as_micros()),
+    }
+    
+    // Demonstrate bucket distribution
+    println!("\n๐Ÿ—‚๏ธ  Cache bucket distribution:");
+    print_bucket_analysis(&packages);
+    
+    println!("\nโœจ Demo complete! The hierarchical cache provides:");
+    println!("   โ€ข L1: Ultra-fast memory access (ฮผs latency)");
+    println!("   โ€ข L2: Fast binary-indexed disk access (ms latency)");
+    println!("   โ€ข L3: Remote registry fallback (s latency, not shown)");
+    println!("   โ€ข Automatic promotion between levels");
+    println!("   โ€ข LRU eviction in L1 memory cache");
+    println!("   โ€ข SHA1-based bucketing for optimal distribution");
+    
+    Ok(())
+}
+
+fn print_cache_stats(cache: &Cache) {
+    let stats = cache.memory_cache_stats();
+    println!("  L1 Memory Cache: {}/{} entries ({:.1}% utilization, {} remaining)",
+        stats.entries, 
+        stats.max_entries,
+        stats.utilization() * 100.0,
+        stats.remaining_capacity());
+}
+
+fn print_bucket_analysis(packages: &[(&str, &str, &str, Vec<String>)]) {
+    use sha1::{Digest, Sha1};
+    
+    for (name, version, pm, _) in packages {
+        let mut hasher = Sha1::new();
+        hasher.update(name.as_bytes());
+        let hash = hasher.finalize();
+        let bucket = format!("{:02x}", hash[0]);
+        
+        println!("  ๐Ÿ“ {}@{} ({}) -> bucket {} (hash: {:02x}{}...)",
+            name, version, pm, bucket, hash[0], 
+            hash.iter().skip(1).take(2).map(|b| format!("{:02x}", b)).collect::<String>());
+    }
+}
\ No newline at end of file
resources/spdx-licenses.json
@@ -0,0 +1,85 @@
+{
+  "licenseListVersion": "3.21",
+  "licenses": [
+    {
+      "licenseId": "MIT",
+      "name": "MIT License",
+      "reference": "https://opensource.org/licenses/MIT",
+      "isOsiApproved": true,
+      "isDeprecatedLicenseId": false,
+      "referenceNumber": 1
+    },
+    {
+      "licenseId": "Apache-2.0",
+      "name": "Apache License 2.0",
+      "reference": "https://www.apache.org/licenses/LICENSE-2.0",
+      "isOsiApproved": true,
+      "isDeprecatedLicenseId": false,
+      "referenceNumber": 2
+    },
+    {
+      "licenseId": "GPL-3.0",
+      "name": "GNU General Public License v3.0",
+      "reference": "https://www.gnu.org/licenses/gpl-3.0.txt",
+      "isOsiApproved": true,
+      "isDeprecatedLicenseId": false,
+      "referenceNumber": 3
+    },
+    {
+      "licenseId": "BSD-3-Clause",
+      "name": "BSD 3-Clause \"New\" or \"Revised\" License",
+      "reference": "https://opensource.org/licenses/BSD-3-Clause",
+      "isOsiApproved": true,
+      "isDeprecatedLicenseId": false,
+      "referenceNumber": 4
+    },
+    {
+      "licenseId": "ISC",
+      "name": "ISC License",
+      "reference": "https://opensource.org/licenses/ISC",
+      "isOsiApproved": true,
+      "isDeprecatedLicenseId": false,
+      "referenceNumber": 5
+    },
+    {
+      "licenseId": "GPL-2.0",
+      "name": "GNU General Public License v2.0",
+      "reference": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt",
+      "isOsiApproved": true,
+      "isDeprecatedLicenseId": false,
+      "referenceNumber": 6
+    },
+    {
+      "licenseId": "LGPL-2.1",
+      "name": "GNU Lesser General Public License v2.1",
+      "reference": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt",
+      "isOsiApproved": true,
+      "isDeprecatedLicenseId": false,
+      "referenceNumber": 7
+    },
+    {
+      "licenseId": "LGPL-3.0",
+      "name": "GNU Lesser General Public License v3.0",
+      "reference": "https://www.gnu.org/licenses/lgpl-3.0.txt",
+      "isOsiApproved": true,
+      "isDeprecatedLicenseId": false,
+      "referenceNumber": 8
+    },
+    {
+      "licenseId": "BSD-2-Clause",
+      "name": "BSD 2-Clause \"Simplified\" License",
+      "reference": "https://opensource.org/licenses/BSD-2-Clause",
+      "isOsiApproved": true,
+      "isDeprecatedLicenseId": false,
+      "referenceNumber": 9
+    },
+    {
+      "licenseId": "MPL-2.0",
+      "name": "Mozilla Public License 2.0",
+      "reference": "https://www.mozilla.org/en-US/MPL/2.0/",
+      "isOsiApproved": true,
+      "isDeprecatedLicenseId": false,
+      "referenceNumber": 10
+    }
+  ]
+}
\ No newline at end of file
src/cache/cache.rs
@@ -0,0 +1,661 @@
+use crate::cache::{DataFile, IndexFile};
+use crate::error::{SpandxError, SpandxResult};
+use camino::{Utf8Path, Utf8PathBuf};
+use sha1::{Digest, Sha1};
+use std::collections::HashMap;
+use tracing::{debug, warn};
+
+/// Cache key for binary-indexed storage
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct CacheKey {
+    pub bucket: String,
+    pub package_manager: String,
+}
+
+impl CacheKey {
+    pub fn new(name: &str, package_manager: &str) -> Self {
+        let mut hasher = Sha1::new();
+        hasher.update(name.as_bytes());
+        let hash = hasher.finalize();
+        let bucket = format!("{:02x}", hash[0]);
+        
+        Self {
+            bucket,
+            package_manager: package_manager.to_string(),
+        }
+    }
+
+    pub fn data_file_path(&self, cache_dir: &Utf8Path) -> Utf8PathBuf {
+        cache_dir
+            .join(".index")
+            .join(&self.bucket)
+            .join(&self.package_manager)
+    }
+
+    pub fn index_file_path(&self, cache_dir: &Utf8Path) -> Utf8PathBuf {
+        cache_dir
+            .join(".index")
+            .join(&self.bucket)
+            .join(format!("{}.idx", self.package_manager))
+    }
+}
+
+/// Entry in the package cache
+#[derive(Debug, Clone, PartialEq)]
+pub struct CacheEntry {
+    pub name: String,
+    pub version: String,
+    pub licenses: Vec<String>,
+}
+
+impl CacheEntry {
+    pub fn new(name: String, version: String, licenses: Vec<String>) -> Self {
+        Self {
+            name,
+            version,
+            licenses,
+        }
+    }
+
+    pub fn to_csv_line(&self) -> String {
+        let licenses_str = if self.licenses.is_empty() {
+            String::new()
+        } else {
+            self.licenses.join("-|-")
+        };
+        format!("\"{}\",\"{}\",\"{}\"", self.name, self.version, licenses_str)
+    }
+
+    pub fn from_csv_line(line: &str) -> SpandxResult<Self> {
+        let mut reader = csv::ReaderBuilder::new()
+            .has_headers(false)
+            .from_reader(line.as_bytes());
+        
+        if let Some(result) = reader.records().next() {
+            let record = result?;
+            if record.len() >= 3 {
+                let name = record[0].to_string();
+                let version = record[1].to_string();
+                let licenses_str = &record[2];
+                
+                let licenses = if licenses_str.is_empty() {
+                    Vec::new()
+                } else {
+                    licenses_str.split("-|-").map(|s| s.to_string()).collect()
+                };
+                
+                return Ok(Self::new(name, version, licenses));
+            }
+        }
+        
+        Err(SpandxError::InvalidFormatError {
+            format: "CSV".to_string(),
+            file_path: "cache entry".to_string(),
+            reason: format!("Invalid CSV line: {}", line),
+        })
+    }
+
+    pub fn key(&self) -> String {
+        format!("{}:{}", self.name, self.version)
+    }
+}
+
+/// Hierarchical binary-indexed package cache with multi-level hierarchy
+#[derive(Debug)]
+pub struct Cache {
+    cache_dir: Utf8PathBuf,
+    data_files: HashMap<CacheKey, DataFile>,
+    index_files: HashMap<CacheKey, IndexFile>,
+    memory_cache: HashMap<String, Vec<String>>, // L1: In-memory cache
+    memory_cache_size: usize,
+    max_memory_entries: usize,
+}
+
+impl Cache {
+    pub fn new(cache_dir: Utf8PathBuf) -> Self {
+        Self::with_memory_cache_size(cache_dir, 1000) // Default 1000 entries
+    }
+
+    pub fn with_memory_cache_size(cache_dir: Utf8PathBuf, max_memory_entries: usize) -> Self {
+        Self {
+            cache_dir,
+            data_files: HashMap::new(),
+            index_files: HashMap::new(),
+            memory_cache: HashMap::new(),
+            memory_cache_size: 0,
+            max_memory_entries,
+        }
+    }
+
+    pub fn cache_dir(&self) -> &Utf8Path {
+        &self.cache_dir
+    }
+
+    /// Get licenses for a package from the hierarchical cache
+    /// L1: Memory cache -> L2: Binary-indexed disk cache -> L3: Fallback lookup
+    pub async fn get_licenses(&mut self, name: &str, version: &str, package_manager: &str) -> SpandxResult<Option<Vec<String>>> {
+        let full_key = format!("{}:{}:{}", package_manager, name, version);
+        
+        // L1: Check memory cache first (fastest)
+        if let Some(licenses) = self.memory_cache.get(&full_key) {
+            debug!("L1 cache hit (memory) for {}@{}", name, version);
+            return Ok(Some(licenses.clone()));
+        }
+        
+        // L2: Check binary-indexed disk cache
+        let cache_key = CacheKey::new(name, package_manager);
+        
+        // Ensure data and index files are loaded
+        self.ensure_files_loaded(&cache_key).await?;
+        
+        let data_file = self.data_files.get(&cache_key);
+        let index_file = self.index_files.get(&cache_key);
+        
+        if let (Some(data_file), Some(index_file)) = (data_file, index_file) {
+            let search_key = format!("{}:{}", name, version);
+            
+            if let Some(offset) = index_file.find_offset(&search_key).await? {
+                if let Some(entry) = data_file.read_entry_at_offset(offset).await? {
+                    if entry.name == name && entry.version == version {
+                        debug!("L2 cache hit (binary-indexed) for {}@{}: {:?}", name, version, entry.licenses);
+                        
+                        // Promote to L1 cache for faster future access
+                        self.add_to_memory_cache(full_key, entry.licenses.clone());
+                        
+                        return Ok(Some(entry.licenses));
+                    }
+                }
+            }
+        }
+        
+        debug!("Cache miss (all levels) for {}@{}", name, version);
+        Ok(None)
+    }
+
+    /// Store licenses for a package in the hierarchical cache
+    /// Stores in both L1 (memory) and L2 (binary-indexed disk) for maximum performance
+    pub async fn set_licenses(&mut self, name: &str, version: &str, package_manager: &str, licenses: Vec<String>) -> SpandxResult<()> {
+        let full_key = format!("{}:{}:{}", package_manager, name, version);
+        let cache_key = CacheKey::new(name, package_manager);
+        let entry = CacheEntry::new(name.to_string(), version.to_string(), licenses.clone());
+        
+        // Store in L1 (memory cache) for immediate access
+        self.add_to_memory_cache(full_key, licenses.clone());
+        
+        // Store in L2 (binary-indexed disk cache) for persistence
+        // Ensure data file is loaded
+        self.ensure_files_loaded(&cache_key).await?;
+        
+        // Append to data file
+        if let Some(data_file) = self.data_files.get_mut(&cache_key) {
+            data_file.append_entry(&entry).await?;
+            debug!("Cached entry in L2 for {}@{}", name, version);
+        } else {
+            // Create new data file
+            let data_path = cache_key.data_file_path(&self.cache_dir);
+            if let Some(parent) = data_path.parent() {
+                tokio::fs::create_dir_all(parent).await?;
+            }
+            
+            let mut data_file = DataFile::create(data_path).await?;
+            data_file.append_entry(&entry).await?;
+            self.data_files.insert(cache_key.clone(), data_file);
+            
+            debug!("Created cache file and cached entry in L2 for {}@{}", name, version);
+        }
+        
+        // Invalidate index to force rebuild on next access
+        self.index_files.remove(&cache_key);
+        
+        Ok(())
+    }
+
+    /// Rebuild index for a package manager
+    pub async fn rebuild_index(&mut self, package_manager: &str) -> SpandxResult<()> {
+        debug!("Rebuilding index for package manager: {}", package_manager);
+        
+        // Rebuild indexes for all buckets that have data files
+        for bucket in 0..=255 {
+            let bucket_str = format!("{:02x}", bucket);
+            let key = CacheKey {
+                bucket: bucket_str,
+                package_manager: package_manager.to_string(),
+            };
+            
+            let data_path = key.data_file_path(&self.cache_dir);
+            if data_path.exists() {
+                self.rebuild_index_for_key(&key).await?;
+            }
+        }
+        
+        debug!("Index rebuild completed for {}", package_manager);
+        Ok(())
+    }
+
+    async fn rebuild_index_for_key(&mut self, key: &CacheKey) -> SpandxResult<()> {
+        let data_path = key.data_file_path(&self.cache_dir);
+        let index_path = key.index_file_path(&self.cache_dir);
+        
+        // Load and sort all entries
+        let mut entries = Vec::new();
+        if let Ok(data_file) = DataFile::open(&data_path).await {
+            let mut all_entries = data_file.read_all_entries().await?;
+            all_entries.sort_by(|a, b| a.key().cmp(&b.key()));
+            all_entries.dedup_by(|a, b| a.key() == b.key());
+            entries = all_entries;
+        }
+        
+        if entries.is_empty() {
+            return Ok(());
+        }
+        
+        // Rewrite sorted data file
+        let mut new_data_file = DataFile::create(&data_path).await?;
+        let mut index_entries = Vec::new();
+        
+        for entry in &entries {
+            let offset = new_data_file.current_offset();
+            new_data_file.append_entry(entry).await?;
+            index_entries.push((entry.key(), offset));
+        }
+        
+        // Create index file
+        let mut index_file = IndexFile::create(index_path).await?;
+        for (key, offset) in index_entries {
+            index_file.add_entry(&key, offset).await?;
+        }
+        index_file.finalize().await?;
+        
+        // Update in-memory references
+        self.data_files.insert(key.clone(), new_data_file);
+        self.index_files.insert(key.clone(), index_file);
+        
+        Ok(())
+    }
+
+    /// Add entry to L1 memory cache with LRU eviction
+    fn add_to_memory_cache(&mut self, key: String, licenses: Vec<String>) {
+        // Simple LRU: remove oldest entry if cache is full
+        if self.memory_cache_size >= self.max_memory_entries {
+            if let Some(first_key) = self.memory_cache.keys().next().cloned() {
+                self.memory_cache.remove(&first_key);
+                self.memory_cache_size -= 1;
+                debug!("Evicted entry from L1 cache: {}", first_key);
+            }
+        }
+        
+        // Remove existing entry if present (for reinsertion at end)
+        if self.memory_cache.remove(&key).is_some() {
+            self.memory_cache_size -= 1;
+        }
+        
+        // Add new entry
+        self.memory_cache.insert(key.clone(), licenses);
+        self.memory_cache_size += 1;
+        debug!("Added entry to L1 cache: {}", key);
+    }
+
+    /// Clear L1 memory cache
+    pub fn clear_memory_cache(&mut self) {
+        self.memory_cache.clear();
+        self.memory_cache_size = 0;
+        debug!("Cleared L1 memory cache");
+    }
+
+    /// Get memory cache statistics
+    pub fn memory_cache_stats(&self) -> MemoryCacheStats {
+        MemoryCacheStats {
+            entries: self.memory_cache_size,
+            max_entries: self.max_memory_entries,
+            hit_rate_estimate: 0.0, // Would need hit/miss counters for real implementation
+        }
+    }
+
+    /// Preload frequently accessed packages into memory cache
+    pub async fn preload_popular_packages(&mut self, package_manager: &str, limit: usize) -> SpandxResult<()> {
+        debug!("Preloading {} popular packages for {}", limit, package_manager);
+        
+        let mut loaded_count = 0;
+        
+        // Iterate through all buckets to find popular packages
+        for bucket in 0..=255 {
+            if loaded_count >= limit {
+                break;
+            }
+            
+            let bucket_str = format!("{:02x}", bucket);
+            let key = CacheKey {
+                bucket: bucket_str,
+                package_manager: package_manager.to_string(),
+            };
+            
+            let data_path = key.data_file_path(&self.cache_dir);
+            if data_path.exists() {
+                if let Ok(data_file) = DataFile::open(&data_path).await {
+                    let entries = data_file.read_all_entries().await?;
+                    
+                    // Load first few entries from each bucket (could be improved with popularity metrics)
+                    for entry in entries.iter().take(limit - loaded_count) {
+                        let full_key = format!("{}:{}:{}", package_manager, entry.name, entry.version);
+                        self.add_to_memory_cache(full_key, entry.licenses.clone());
+                        loaded_count += 1;
+                        
+                        if loaded_count >= limit {
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        
+        debug!("Preloaded {} packages into L1 cache", loaded_count);
+        Ok(())
+    }
+
+    async fn ensure_files_loaded(&mut self, key: &CacheKey) -> SpandxResult<()> {
+        if !self.data_files.contains_key(key) {
+            let data_path = key.data_file_path(&self.cache_dir);
+            debug!("Loading data file: {:?}", data_path);
+            if data_path.exists() {
+                match DataFile::open(&data_path).await {
+                    Ok(data_file) => {
+                        self.data_files.insert(key.clone(), data_file);
+                        debug!("Successfully loaded data file");
+                    }
+                    Err(e) => {
+                        warn!("Failed to open data file {:?}: {}", data_path, e);
+                    }
+                }
+            } else {
+                debug!("Data file does not exist: {:?}", data_path);
+            }
+        }
+        
+        if !self.index_files.contains_key(key) {
+            let index_path = key.index_file_path(&self.cache_dir);
+            debug!("Loading index file: {:?}", index_path);
+            if index_path.exists() {
+                match IndexFile::open(index_path).await {
+                    Ok(index_file) => {
+                        let entries_count = index_file.len();
+                        self.index_files.insert(key.clone(), index_file);
+                        debug!("Successfully loaded index file with {} entries", entries_count);
+                    }
+                    Err(e) => {
+                        warn!("Failed to open index file, will rebuild: {}", e);
+                        // Try to rebuild index if it's corrupted
+                        self.rebuild_index_for_key(key).await?;
+                    }
+                }
+            } else {
+                debug!("Index file does not exist, rebuilding: {:?}", index_path);
+                // Index doesn't exist, try to rebuild from data file
+                let data_path = key.data_file_path(&self.cache_dir);
+                if data_path.exists() {
+                    self.rebuild_index_for_key(key).await?;
+                }
+            }
+        }
+        
+        Ok(())
+    }
+
+    /// Get cache statistics
+    pub async fn stats(&mut self, package_manager: &str) -> SpandxResult<CacheStats> {
+        let mut total_entries = 0;
+        let mut total_buckets = 0;
+        
+        for bucket in 0..=255 {
+            let bucket_str = format!("{:02x}", bucket);
+            let key = CacheKey {
+                bucket: bucket_str,
+                package_manager: package_manager.to_string(),
+            };
+            
+            let data_path = key.data_file_path(&self.cache_dir);
+            if data_path.exists() {
+                total_buckets += 1;
+                if let Ok(data_file) = DataFile::open(&data_path).await {
+                    total_entries += data_file.count_entries().await?;
+                }
+            }
+        }
+        
+        Ok(CacheStats {
+            total_entries,
+            total_buckets,
+            package_manager: package_manager.to_string(),
+        })
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct CacheStats {
+    pub total_entries: usize,
+    pub total_buckets: usize,
+    pub package_manager: String,
+}
+
+impl CacheStats {
+    pub fn avg_entries_per_bucket(&self) -> f64 {
+        if self.total_buckets == 0 {
+            0.0
+        } else {
+            self.total_entries as f64 / self.total_buckets as f64
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct MemoryCacheStats {
+    pub entries: usize,
+    pub max_entries: usize,
+    pub hit_rate_estimate: f64,
+}
+
+impl MemoryCacheStats {
+    pub fn utilization(&self) -> f64 {
+        if self.max_entries == 0 {
+            0.0
+        } else {
+            self.entries as f64 / self.max_entries as f64
+        }
+    }
+
+    pub fn remaining_capacity(&self) -> usize {
+        self.max_entries.saturating_sub(self.entries)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use tempfile::TempDir;
+
+    #[test]
+    fn test_cache_key_generation() {
+        let key1 = CacheKey::new("rails", "rubygems");
+        let key2 = CacheKey::new("rails", "rubygems");
+        let key3 = CacheKey::new("django", "python");
+        
+        assert_eq!(key1, key2);
+        assert_ne!(key1, key3);
+        assert_eq!(key1.package_manager, "rubygems");
+        assert_eq!(key3.package_manager, "python");
+    }
+
+    #[test]
+    fn test_cache_entry_csv() {
+        let entry = CacheEntry::new(
+            "rails".to_string(),
+            "7.0.0".to_string(),
+            vec!["MIT".to_string(), "Apache-2.0".to_string()],
+        );
+        
+        let csv_line = entry.to_csv_line();
+        assert_eq!(csv_line, "\"rails\",\"7.0.0\",\"MIT-|-Apache-2.0\"");
+        
+        let parsed_entry = CacheEntry::from_csv_line(&csv_line).unwrap();
+        assert_eq!(parsed_entry, entry);
+    }
+
+    #[test]
+    fn test_cache_entry_empty_licenses() {
+        let entry = CacheEntry::new(
+            "unknown".to_string(),
+            "1.0.0".to_string(),
+            vec![],
+        );
+        
+        let csv_line = entry.to_csv_line();
+        assert_eq!(csv_line, "\"unknown\",\"1.0.0\",\"\"");
+        
+        let parsed_entry = CacheEntry::from_csv_line(&csv_line).unwrap();
+        assert_eq!(parsed_entry, entry);
+    }
+
+    #[tokio::test]
+    async fn test_cache_basic_operations() {
+        let temp_dir = TempDir::new().unwrap();
+        let cache_dir = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).unwrap();
+        let mut cache = Cache::new(cache_dir);
+
+        // Test cache miss
+        let result = cache.get_licenses("rails", "7.0.0", "rubygems").await.unwrap();
+        assert!(result.is_none());
+
+        // Set licenses
+        let licenses = vec!["MIT".to_string(), "Apache-2.0".to_string()];
+        cache.set_licenses("rails", "7.0.0", "rubygems", licenses.clone()).await.unwrap();
+
+        // Test cache hit (should come from L1 memory cache now)
+        let result = cache.get_licenses("rails", "7.0.0", "rubygems").await.unwrap();
+        assert_eq!(result, Some(licenses));
+
+        // Test different version (cache miss)
+        let result = cache.get_licenses("rails", "6.0.0", "rubygems").await.unwrap();
+        assert!(result.is_none());
+    }
+
+    #[tokio::test]
+    async fn test_cache_stats() {
+        let temp_dir = TempDir::new().unwrap();
+        let cache_dir = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).unwrap();
+        let mut cache = Cache::new(cache_dir);
+
+        // Add some entries
+        cache.set_licenses("rails", "7.0.0", "rubygems", vec!["MIT".to_string()]).await.unwrap();
+        cache.set_licenses("sinatra", "2.0.0", "rubygems", vec!["MIT".to_string()]).await.unwrap();
+
+        let stats = cache.stats("rubygems").await.unwrap();
+        assert!(stats.total_entries >= 2);
+        assert!(stats.total_buckets >= 1);
+        assert_eq!(stats.package_manager, "rubygems");
+    }
+
+    #[tokio::test]
+    async fn test_hierarchical_cache_levels() {
+        let temp_dir = TempDir::new().unwrap();
+        let cache_dir = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).unwrap();
+        let mut cache = Cache::with_memory_cache_size(cache_dir, 2); // Small L1 cache for testing
+
+        let licenses = vec!["MIT".to_string()];
+        
+        // Store in cache (goes to both L1 and L2)
+        cache.set_licenses("rails", "7.0.0", "rubygems", licenses.clone()).await.unwrap();
+        
+        // Verify L1 cache stats
+        let memory_stats = cache.memory_cache_stats();
+        assert_eq!(memory_stats.entries, 1);
+        assert_eq!(memory_stats.max_entries, 2);
+        assert_eq!(memory_stats.utilization(), 0.5);
+        
+        // First retrieval should hit L1 cache
+        let result = cache.get_licenses("rails", "7.0.0", "rubygems").await.unwrap();
+        assert_eq!(result, Some(licenses.clone()));
+        
+        // Clear L1 cache to test L2 fallback
+        cache.clear_memory_cache();
+        assert_eq!(cache.memory_cache_stats().entries, 0);
+        
+        // Second retrieval should hit L2 cache and promote to L1
+        let result = cache.get_licenses("rails", "7.0.0", "rubygems").await.unwrap();
+        assert_eq!(result, Some(licenses));
+        assert_eq!(cache.memory_cache_stats().entries, 1); // Promoted back to L1
+    }
+
+    #[tokio::test]
+    async fn test_memory_cache_lru_eviction() {
+        let temp_dir = TempDir::new().unwrap();
+        let cache_dir = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).unwrap();
+        let mut cache = Cache::with_memory_cache_size(cache_dir, 2); // Only 2 entries
+
+        let licenses = vec!["MIT".to_string()];
+        
+        // Fill L1 cache to capacity
+        cache.set_licenses("pkg1", "1.0.0", "npm", licenses.clone()).await.unwrap();
+        cache.set_licenses("pkg2", "1.0.0", "npm", licenses.clone()).await.unwrap();
+        assert_eq!(cache.memory_cache_stats().entries, 2);
+        
+        // Add third entry, should evict first
+        cache.set_licenses("pkg3", "1.0.0", "npm", licenses.clone()).await.unwrap();
+        assert_eq!(cache.memory_cache_stats().entries, 2); // Still 2 entries
+        
+        // pkg1 should be evicted from L1, but still available in L2
+        let result = cache.get_licenses("pkg1", "1.0.0", "npm").await.unwrap();
+        assert_eq!(result, Some(licenses.clone())); // Should hit L2 and promote to L1
+        
+        // pkg2 should now be evicted from L1 due to LRU
+        let result = cache.get_licenses("pkg2", "1.0.0", "npm").await.unwrap();
+        assert_eq!(result, Some(licenses)); // Should hit L2
+    }
+
+    #[tokio::test]
+    async fn test_preload_popular_packages() {
+        let temp_dir = TempDir::new().unwrap();
+        let cache_dir = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).unwrap();
+        let mut cache = Cache::with_memory_cache_size(cache_dir, 10);
+
+        let licenses = vec!["MIT".to_string()];
+        
+        // Add some packages to L2 cache
+        cache.set_licenses("popular1", "1.0.0", "npm", licenses.clone()).await.unwrap();
+        cache.set_licenses("popular2", "2.0.0", "npm", licenses.clone()).await.unwrap();
+        cache.set_licenses("popular3", "3.0.0", "npm", licenses.clone()).await.unwrap();
+        
+        // Clear L1 to test preloading
+        cache.clear_memory_cache();
+        assert_eq!(cache.memory_cache_stats().entries, 0);
+        
+        // Preload popular packages
+        cache.preload_popular_packages("npm", 5).await.unwrap();
+        
+        // Should have loaded some packages into L1
+        let stats = cache.memory_cache_stats();
+        assert!(stats.entries > 0);
+        assert!(stats.entries <= 5);
+    }
+
+    #[tokio::test]
+    async fn test_memory_cache_stats() {
+        let temp_dir = TempDir::new().unwrap();
+        let cache_dir = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).unwrap();
+        let mut cache = Cache::with_memory_cache_size(cache_dir, 5);
+
+        let stats = cache.memory_cache_stats();
+        assert_eq!(stats.entries, 0);
+        assert_eq!(stats.max_entries, 5);
+        assert_eq!(stats.utilization(), 0.0);
+        assert_eq!(stats.remaining_capacity(), 5);
+        
+        // Add some entries
+        let licenses = vec!["MIT".to_string()];
+        cache.set_licenses("pkg1", "1.0.0", "npm", licenses.clone()).await.unwrap();
+        cache.set_licenses("pkg2", "1.0.0", "npm", licenses).await.unwrap();
+        
+        let stats = cache.memory_cache_stats();
+        assert_eq!(stats.entries, 2);
+        assert_eq!(stats.utilization(), 0.4); // 2/5 = 0.4
+        assert_eq!(stats.remaining_capacity(), 3);
+    }
+}
\ No newline at end of file
src/cache/data_file.rs
@@ -0,0 +1,307 @@
+use crate::cache::cache::CacheEntry;
+use crate::error::{SpandxError, SpandxResult};
+use camino::{Utf8Path, Utf8PathBuf};
+use tokio::fs::{File, OpenOptions};
+use tokio::io::{AsyncBufReadExt, AsyncSeekExt, AsyncWriteExt, BufReader};
+use tracing::{debug, warn};
+
+/// Handles CSV data files containing package information
+#[derive(Debug)]
+pub struct DataFile {
+    path: Utf8PathBuf,
+    file: Option<File>,
+    current_offset: u64,
+}
+
+impl DataFile {
+    pub async fn create<P: AsRef<Utf8Path>>(path: P) -> SpandxResult<Self> {
+        let path = path.as_ref().to_path_buf();
+        
+        // Ensure parent directory exists
+        if let Some(parent) = path.parent() {
+            tokio::fs::create_dir_all(parent).await?;
+        }
+        
+        let file = OpenOptions::new()
+            .create(true)
+            .write(true)
+            .truncate(true)
+            .open(&path)
+            .await?;
+            
+        Ok(Self {
+            path,
+            file: Some(file),
+            current_offset: 0,
+        })
+    }
+
+    pub async fn open<P: AsRef<Utf8Path>>(path: P) -> SpandxResult<Self> {
+        let path = path.as_ref().to_path_buf();
+        
+        let file = OpenOptions::new()
+            .read(true)
+            .write(true)
+            .open(&path)
+            .await?;
+            
+        Ok(Self {
+            path,
+            file: Some(file),
+            current_offset: 0,
+        })
+    }
+
+    pub fn current_offset(&self) -> u64 {
+        self.current_offset
+    }
+
+    pub async fn append_entry(&mut self, entry: &CacheEntry) -> SpandxResult<()> {
+        if let Some(ref mut file) = self.file {
+            let csv_line = entry.to_csv_line();
+            let line_with_newline = format!("{}\n", csv_line);
+            
+            file.write_all(line_with_newline.as_bytes()).await?;
+            file.flush().await?;
+            
+            self.current_offset += line_with_newline.len() as u64;
+            
+            debug!("Appended entry to {}: {}", self.path, csv_line);
+        } else {
+            return Err(SpandxError::CacheError {
+                operation: "append_entry".to_string(),
+                source: Some(Box::new(std::io::Error::new(
+                    std::io::ErrorKind::InvalidInput,
+                    "Data file not open for writing"
+                )))
+            });
+        }
+        
+        Ok(())
+    }
+
+    pub async fn read_entry_at_offset(&self, offset: u64) -> SpandxResult<Option<CacheEntry>> {
+        let file = File::open(&self.path).await?;
+        let mut reader = BufReader::new(file);
+        
+        reader.seek(std::io::SeekFrom::Start(offset)).await?;
+        
+        let mut line = String::new();
+        let bytes_read = reader.read_line(&mut line).await?;
+        
+        if bytes_read == 0 {
+            return Ok(None);
+        }
+        
+        // Remove trailing newline
+        if line.ends_with('\n') {
+            line.pop();
+        }
+        if line.ends_with('\r') {
+            line.pop();
+        }
+        
+        match CacheEntry::from_csv_line(&line) {
+            Ok(entry) => Ok(Some(entry)),
+            Err(e) => {
+                warn!("Failed to parse CSV line at offset {}: {} - {}", offset, line, e);
+                Ok(None)
+            }
+        }
+    }
+
+    pub async fn read_all_entries(&self) -> SpandxResult<Vec<CacheEntry>> {
+        let file = File::open(&self.path).await?;
+        let reader = BufReader::new(file);
+        let mut lines = reader.lines();
+        let mut entries = Vec::new();
+        
+        while let Some(line) = lines.next_line().await? {
+            if !line.trim().is_empty() {
+                match CacheEntry::from_csv_line(&line) {
+                    Ok(entry) => entries.push(entry),
+                    Err(e) => {
+                        warn!("Failed to parse CSV line: {} - {}", line, e);
+                    }
+                }
+            }
+        }
+        
+        Ok(entries)
+    }
+
+    pub async fn count_entries(&self) -> SpandxResult<usize> {
+        let file = File::open(&self.path).await?;
+        let reader = BufReader::new(file);
+        let mut lines = reader.lines();
+        let mut count = 0;
+        
+        while let Some(line) = lines.next_line().await? {
+            if !line.trim().is_empty() {
+                count += 1;
+            }
+        }
+        
+        Ok(count)
+    }
+
+    pub async fn iterate_entries<F>(&self, mut callback: F) -> SpandxResult<()>
+    where
+        F: FnMut(&CacheEntry) -> bool, // Return false to stop iteration
+    {
+        let file = File::open(&self.path).await?;
+        let reader = BufReader::new(file);
+        let mut lines = reader.lines();
+        
+        while let Some(line) = lines.next_line().await? {
+            if !line.trim().is_empty() {
+                match CacheEntry::from_csv_line(&line) {
+                    Ok(entry) => {
+                        if !callback(&entry) {
+                            break;
+                        }
+                    }
+                    Err(e) => {
+                        warn!("Failed to parse CSV line during iteration: {} - {}", line, e);
+                    }
+                }
+            }
+        }
+        
+        Ok(())
+    }
+
+    pub fn path(&self) -> &Utf8Path {
+        &self.path
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use tempfile::TempDir;
+
+    #[tokio::test]
+    async fn test_data_file_create_and_append() {
+        let temp_dir = TempDir::new().unwrap();
+        let file_path = Utf8PathBuf::from_path_buf(temp_dir.path().join("test.csv")).unwrap();
+        
+        let mut data_file = DataFile::create(&file_path).await.unwrap();
+        
+        let entry = CacheEntry::new(
+            "rails".to_string(),
+            "7.0.0".to_string(),
+            vec!["MIT".to_string()],
+        );
+        
+        let initial_offset = data_file.current_offset();
+        data_file.append_entry(&entry).await.unwrap();
+        
+        assert!(data_file.current_offset() > initial_offset);
+        assert!(file_path.exists());
+    }
+
+    #[tokio::test]
+    async fn test_data_file_read_entry_at_offset() {
+        let temp_dir = TempDir::new().unwrap();
+        let file_path = Utf8PathBuf::from_path_buf(temp_dir.path().join("test.csv")).unwrap();
+        
+        let mut data_file = DataFile::create(&file_path).await.unwrap();
+        
+        let entry1 = CacheEntry::new(
+            "rails".to_string(),
+            "7.0.0".to_string(),
+            vec!["MIT".to_string()],
+        );
+        let entry2 = CacheEntry::new(
+            "sinatra".to_string(),
+            "2.0.0".to_string(),
+            vec!["MIT".to_string(), "Apache-2.0".to_string()],
+        );
+        
+        let offset1 = data_file.current_offset();
+        data_file.append_entry(&entry1).await.unwrap();
+        
+        let offset2 = data_file.current_offset();
+        data_file.append_entry(&entry2).await.unwrap();
+        
+        // Read entries back
+        let read_entry1 = data_file.read_entry_at_offset(offset1).await.unwrap().unwrap();
+        let read_entry2 = data_file.read_entry_at_offset(offset2).await.unwrap().unwrap();
+        
+        assert_eq!(read_entry1, entry1);
+        assert_eq!(read_entry2, entry2);
+    }
+
+    #[tokio::test]
+    async fn test_data_file_read_all_entries() {
+        let temp_dir = TempDir::new().unwrap();
+        let file_path = Utf8PathBuf::from_path_buf(temp_dir.path().join("test.csv")).unwrap();
+        
+        let mut data_file = DataFile::create(&file_path).await.unwrap();
+        
+        let entries = vec![
+            CacheEntry::new("rails".to_string(), "7.0.0".to_string(), vec!["MIT".to_string()]),
+            CacheEntry::new("sinatra".to_string(), "2.0.0".to_string(), vec!["MIT".to_string()]),
+            CacheEntry::new("rack".to_string(), "2.0.0".to_string(), vec!["MIT".to_string()]),
+        ];
+        
+        for entry in &entries {
+            data_file.append_entry(entry).await.unwrap();
+        }
+        
+        let read_entries = data_file.read_all_entries().await.unwrap();
+        assert_eq!(read_entries, entries);
+    }
+
+    #[tokio::test]
+    async fn test_data_file_count_entries() {
+        let temp_dir = TempDir::new().unwrap();
+        let file_path = Utf8PathBuf::from_path_buf(temp_dir.path().join("test.csv")).unwrap();
+        
+        let mut data_file = DataFile::create(&file_path).await.unwrap();
+        
+        assert_eq!(data_file.count_entries().await.unwrap(), 0);
+        
+        data_file.append_entry(&CacheEntry::new("rails".to_string(), "7.0.0".to_string(), vec!["MIT".to_string()])).await.unwrap();
+        assert_eq!(data_file.count_entries().await.unwrap(), 1);
+        
+        data_file.append_entry(&CacheEntry::new("sinatra".to_string(), "2.0.0".to_string(), vec!["MIT".to_string()])).await.unwrap();
+        assert_eq!(data_file.count_entries().await.unwrap(), 2);
+    }
+
+    #[tokio::test]
+    async fn test_data_file_iterate_entries() {
+        let temp_dir = TempDir::new().unwrap();
+        let file_path = Utf8PathBuf::from_path_buf(temp_dir.path().join("test.csv")).unwrap();
+        
+        let mut data_file = DataFile::create(&file_path).await.unwrap();
+        
+        let entries = vec![
+            CacheEntry::new("rails".to_string(), "7.0.0".to_string(), vec!["MIT".to_string()]),
+            CacheEntry::new("sinatra".to_string(), "2.0.0".to_string(), vec!["MIT".to_string()]),
+            CacheEntry::new("rack".to_string(), "2.0.0".to_string(), vec!["MIT".to_string()]),
+        ];
+        
+        for entry in &entries {
+            data_file.append_entry(entry).await.unwrap();
+        }
+        
+        let mut collected_entries = Vec::new();
+        data_file.iterate_entries(|entry| {
+            collected_entries.push(entry.clone());
+            true // Continue iteration
+        }).await.unwrap();
+        
+        assert_eq!(collected_entries, entries);
+        
+        // Test early termination
+        let mut limited_entries = Vec::new();
+        data_file.iterate_entries(|entry| {
+            limited_entries.push(entry.clone());
+            limited_entries.len() < 2 // Stop after 2 entries
+        }).await.unwrap();
+        
+        assert_eq!(limited_entries.len(), 2);
+    }
+}
\ No newline at end of file
src/cache/index.rs
@@ -0,0 +1,51 @@
+use anyhow::Result;
+use camino::Utf8Path;
+use tracing::warn;
+
+use super::CacheManager;
+
+pub struct IndexBuilder<'a> {
+    #[allow(dead_code)]
+    directory: &'a Utf8Path,
+}
+
+impl<'a> IndexBuilder<'a> {
+    pub fn new(directory: &'a Utf8Path) -> Self {
+        Self { directory }
+    }
+
+    pub async fn build_spdx_index(&self, _cache_manager: &CacheManager) -> Result<()> {
+        warn!("SPDX index building not yet implemented");
+        Ok(())
+    }
+
+    pub async fn build_rubygems_index(&self, _cache_manager: &CacheManager) -> Result<()> {
+        warn!("Ruby gems index building not yet implemented");
+        Ok(())
+    }
+
+    pub async fn build_npm_index(&self, _cache_manager: &CacheManager) -> Result<()> {
+        warn!("NPM index building not yet implemented");
+        Ok(())
+    }
+
+    pub async fn build_pypi_index(&self, _cache_manager: &CacheManager) -> Result<()> {
+        warn!("PyPI index building not yet implemented");
+        Ok(())
+    }
+
+    pub async fn build_nuget_index(&self, _cache_manager: &CacheManager) -> Result<()> {
+        warn!("NuGet index building not yet implemented");
+        Ok(())
+    }
+
+    pub async fn build_maven_index(&self, _cache_manager: &CacheManager) -> Result<()> {
+        warn!("Maven index building not yet implemented");
+        Ok(())
+    }
+
+    pub async fn build_packagist_index(&self, _cache_manager: &CacheManager) -> Result<()> {
+        warn!("Packagist index building not yet implemented");
+        Ok(())
+    }
+}
\ No newline at end of file
src/cache/index_file.rs
@@ -0,0 +1,268 @@
+use crate::error::SpandxResult;
+use camino::{Utf8Path, Utf8PathBuf};
+use std::collections::BTreeMap;
+use tokio::fs::{File, OpenOptions};
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+use tracing::debug;
+
+/// Binary index file for fast lookups in data files
+#[derive(Debug)]
+pub struct IndexFile {
+    path: Utf8PathBuf,
+    entries: BTreeMap<String, u64>, // key -> offset
+    is_dirty: bool,
+}
+
+impl IndexFile {
+    pub async fn create<P: AsRef<Utf8Path>>(path: P) -> SpandxResult<Self> {
+        let path = path.as_ref().to_path_buf();
+        
+        // Ensure parent directory exists
+        if let Some(parent) = path.parent() {
+            tokio::fs::create_dir_all(parent).await?;
+        }
+        
+        Ok(Self {
+            path,
+            entries: BTreeMap::new(),
+            is_dirty: false,
+        })
+    }
+
+    pub async fn open<P: AsRef<Utf8Path>>(path: P) -> SpandxResult<Self> {
+        let path = path.as_ref().to_path_buf();
+        let mut entries = BTreeMap::new();
+        
+        if path.exists() {
+            let mut file = File::open(&path).await?;
+            
+            // Read the number of entries (4 bytes, little-endian)
+            let mut count_bytes = [0u8; 4];
+            file.read_exact(&mut count_bytes).await?;
+            let entry_count = u32::from_le_bytes(count_bytes) as usize;
+            
+            debug!("Loading index file {} with {} entries", path, entry_count);
+            
+            // Read each entry: key_length (4 bytes) + key + offset (8 bytes)
+            for _ in 0..entry_count {
+                // Read key length
+                let mut key_len_bytes = [0u8; 4];
+                file.read_exact(&mut key_len_bytes).await?;
+                let key_len = u32::from_le_bytes(key_len_bytes) as usize;
+                
+                // Read key
+                let mut key_bytes = vec![0u8; key_len];
+                file.read_exact(&mut key_bytes).await?;
+                let key = String::from_utf8(key_bytes)?;
+                
+                // Read offset
+                let mut offset_bytes = [0u8; 8];
+                file.read_exact(&mut offset_bytes).await?;
+                let offset = u64::from_le_bytes(offset_bytes);
+                
+                entries.insert(key, offset);
+            }
+        }
+        
+        Ok(Self {
+            path,
+            entries,
+            is_dirty: false,
+        })
+    }
+
+    pub async fn add_entry(&mut self, key: &str, offset: u64) -> SpandxResult<()> {
+        self.entries.insert(key.to_string(), offset);
+        self.is_dirty = true;
+        Ok(())
+    }
+
+    pub async fn find_offset(&self, key: &str) -> SpandxResult<Option<u64>> {
+        Ok(self.entries.get(key).copied())
+    }
+
+    pub async fn finalize(&mut self) -> SpandxResult<()> {
+        if !self.is_dirty {
+            return Ok(());
+        }
+        
+        let mut file = OpenOptions::new()
+            .create(true)
+            .write(true)
+            .truncate(true)
+            .open(&self.path)
+            .await?;
+        
+        // Write number of entries
+        let entry_count = self.entries.len() as u32;
+        file.write_all(&entry_count.to_le_bytes()).await?;
+        
+        // Write each entry
+        for (key, offset) in &self.entries {
+            // Write key length
+            let key_bytes = key.as_bytes();
+            let key_len = key_bytes.len() as u32;
+            file.write_all(&key_len.to_le_bytes()).await?;
+            
+            // Write key
+            file.write_all(key_bytes).await?;
+            
+            // Write offset
+            file.write_all(&offset.to_le_bytes()).await?;
+        }
+        
+        file.flush().await?;
+        self.is_dirty = false;
+        
+        debug!("Finalized index file {} with {} entries", self.path, self.entries.len());
+        Ok(())
+    }
+
+    pub fn len(&self) -> usize {
+        self.entries.len()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.entries.is_empty()
+    }
+
+    pub fn keys(&self) -> impl Iterator<Item = &String> {
+        self.entries.keys()
+    }
+
+    pub fn path(&self) -> &Utf8Path {
+        &self.path
+    }
+
+    /// Get range of keys for binary search optimization
+    pub fn key_range(&self) -> Option<(&str, &str)> {
+        if self.entries.is_empty() {
+            None
+        } else {
+            let first_key = self.entries.keys().next().unwrap();
+            let last_key = self.entries.keys().last().unwrap();
+            Some((first_key, last_key))
+        }
+    }
+
+    /// Find all keys with a given prefix
+    pub fn find_keys_with_prefix(&self, prefix: &str) -> Vec<&String> {
+        self.entries
+            .keys()
+            .filter(|key| key.starts_with(prefix))
+            .collect()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use tempfile::TempDir;
+
+    #[tokio::test]
+    async fn test_index_file_create_and_add() {
+        let temp_dir = TempDir::new().unwrap();
+        let file_path = Utf8PathBuf::from_path_buf(temp_dir.path().join("test.idx")).unwrap();
+        
+        let mut index_file = IndexFile::create(&file_path).await.unwrap();
+        
+        index_file.add_entry("rails:7.0.0", 0).await.unwrap();
+        index_file.add_entry("sinatra:2.0.0", 42).await.unwrap();
+        index_file.add_entry("rack:2.0.0", 100).await.unwrap();
+        
+        assert_eq!(index_file.len(), 3);
+        assert!(!index_file.is_empty());
+        
+        let offset = index_file.find_offset("sinatra:2.0.0").await.unwrap();
+        assert_eq!(offset, Some(42));
+        
+        let no_offset = index_file.find_offset("unknown:1.0.0").await.unwrap();
+        assert_eq!(no_offset, None);
+    }
+
+    #[tokio::test]
+    async fn test_index_file_finalize_and_reload() {
+        let temp_dir = TempDir::new().unwrap();
+        let file_path = Utf8PathBuf::from_path_buf(temp_dir.path().join("test.idx")).unwrap();
+        
+        // Create and populate index
+        {
+            let mut index_file = IndexFile::create(&file_path).await.unwrap();
+            index_file.add_entry("rails:7.0.0", 0).await.unwrap();
+            index_file.add_entry("sinatra:2.0.0", 42).await.unwrap();
+            index_file.add_entry("rack:2.0.0", 100).await.unwrap();
+            index_file.finalize().await.unwrap();
+        }
+        
+        // Reload and verify
+        {
+            let index_file = IndexFile::open(&file_path).await.unwrap();
+            assert_eq!(index_file.len(), 3);
+            
+            let offset = index_file.find_offset("sinatra:2.0.0").await.unwrap();
+            assert_eq!(offset, Some(42));
+            
+            let offset = index_file.find_offset("rack:2.0.0").await.unwrap();
+            assert_eq!(offset, Some(100));
+        }
+    }
+
+    #[tokio::test]
+    async fn test_index_file_sorted_order() {
+        let temp_dir = TempDir::new().unwrap();
+        let file_path = Utf8PathBuf::from_path_buf(temp_dir.path().join("test.idx")).unwrap();
+        
+        let mut index_file = IndexFile::create(&file_path).await.unwrap();
+        
+        // Add entries in non-alphabetical order
+        index_file.add_entry("zebra:1.0.0", 200).await.unwrap();
+        index_file.add_entry("apple:1.0.0", 0).await.unwrap();
+        index_file.add_entry("banana:1.0.0", 100).await.unwrap();
+        
+        let keys: Vec<&String> = index_file.keys().collect();
+        assert_eq!(keys, vec!["apple:1.0.0", "banana:1.0.0", "zebra:1.0.0"]);
+        
+        let range = index_file.key_range().unwrap();
+        assert_eq!(range, ("apple:1.0.0", "zebra:1.0.0"));
+    }
+
+    #[tokio::test]
+    async fn test_index_file_prefix_search() {
+        let temp_dir = TempDir::new().unwrap();
+        let file_path = Utf8PathBuf::from_path_buf(temp_dir.path().join("test.idx")).unwrap();
+        
+        let mut index_file = IndexFile::create(&file_path).await.unwrap();
+        
+        index_file.add_entry("rails:6.0.0", 0).await.unwrap();
+        index_file.add_entry("rails:7.0.0", 50).await.unwrap();
+        index_file.add_entry("rake:13.0.0", 100).await.unwrap();
+        index_file.add_entry("sinatra:2.0.0", 150).await.unwrap();
+        
+        let rails_keys = index_file.find_keys_with_prefix("rails:");
+        assert_eq!(rails_keys.len(), 2);
+        assert!(rails_keys.contains(&&"rails:6.0.0".to_string()));
+        assert!(rails_keys.contains(&&"rails:7.0.0".to_string()));
+        
+        let sinatra_keys = index_file.find_keys_with_prefix("sinatra:");
+        assert_eq!(sinatra_keys.len(), 1);
+        assert!(sinatra_keys.contains(&&"sinatra:2.0.0".to_string()));
+        
+        let unknown_keys = index_file.find_keys_with_prefix("unknown:");
+        assert_eq!(unknown_keys.len(), 0);
+    }
+
+    #[tokio::test]
+    async fn test_index_file_empty() {
+        let temp_dir = TempDir::new().unwrap();
+        let file_path = Utf8PathBuf::from_path_buf(temp_dir.path().join("empty.idx")).unwrap();
+        
+        let index_file = IndexFile::create(&file_path).await.unwrap();
+        
+        assert!(index_file.is_empty());
+        assert_eq!(index_file.len(), 0);
+        assert!(index_file.key_range().is_none());
+        
+        let no_offset = index_file.find_offset("anything").await.unwrap();
+        assert_eq!(no_offset, None);
+    }
+}
\ No newline at end of file
src/cache/manager.rs
@@ -0,0 +1,276 @@
+use crate::error::{SpandxError, SpandxResult};
+use tracing::{info, warn};
+
+use crate::cache::{Cache, CacheStats, MemoryCacheStats};
+use crate::git::GitOperations;
+
+pub struct CacheManager {
+    git_operations: GitOperations,
+    cache: Cache,
+}
+
+impl CacheManager {
+    pub async fn new() -> SpandxResult<Self> {
+        // Load Git configuration
+        let git_config = crate::git::config::load_config_with_defaults().await?;
+        
+        // Create repositories from configuration
+        let repositories = git_config.create_repositories()?;
+        let git_operations = GitOperations::new(repositories);
+        
+        // Initialize hierarchical cache with reasonable memory cache size
+        let cache_dir = git_config.get_base_path()?.join("cache");
+        let cache = Cache::with_memory_cache_size(cache_dir, 5000); // 5000 entries in L1 cache
+        
+        Ok(Self {
+            git_operations,
+            cache,
+        })
+    }
+    
+    /// Update all Git repositories and rebuild cache indices
+    pub async fn update_all(&mut self) -> SpandxResult<()> {
+        info!("Starting comprehensive cache update...");
+        
+        // Update all Git repositories
+        let update_result = self.git_operations.update_all().await?;
+        
+        if !update_result.is_success() {
+            warn!("Some repositories failed to update:");
+            for (repo, error) in &update_result.failed {
+                warn!("  {}: {}", repo, error);
+            }
+            
+            if update_result.successful.is_empty() {
+                return Err(SpandxError::GitError {
+                    operation: "update_all".to_string(),
+                    repository: "multiple".to_string(),
+                    source: git2::Error::from_str("All repository updates failed"),
+                });
+            }
+        }
+        
+        info!("Successfully updated {} repositories", update_result.successful.len());
+        
+        // Rebuild cache indices from updated repositories
+        let build_result = self.git_operations.build_cache_indices(&mut self.cache).await?;
+        
+        if !build_result.is_success() {
+            warn!("Some cache builds failed:");
+            for (repo, error) in &build_result.errors {
+                warn!("  {}: {}", repo, error);
+            }
+        }
+        
+        info!("Cache update complete. Total entries: {}", build_result.total_entries());
+        Ok(())
+    }
+
+    pub async fn update_spdx_cache(&mut self) -> SpandxResult<()> {
+        info!("Updating SPDX cache...");
+        
+        // Update only the SPDX repository
+        if let Err(e) = self.git_operations.update_repository("spdx").await {
+            warn!("Failed to update SPDX repository: {}", e);
+            return Err(e.into());
+        }
+        
+        info!("SPDX cache updated successfully");
+        Ok(())
+    }
+
+    pub async fn update_rubygems_cache(&mut self) -> SpandxResult<()> {
+        info!("Updating Ruby gems cache...");
+        
+        // Update only the RubyGems repository
+        if let Err(e) = self.git_operations.update_repository("rubygems").await {
+            warn!("Failed to update RubyGems repository: {}", e);
+            return Err(e.into());
+        }
+        
+        // Rebuild cache for RubyGems
+        let cache_dir = self.git_operations
+            .get_repository("rubygems")
+            .ok_or_else(|| SpandxError::GitRepositoryNotFound {
+                path: "rubygems".to_string()
+            })?
+            .cache_index_dir();
+            
+        if cache_dir.exists() {
+            self.cache.rebuild_index("rubygems").await?;
+            info!("RubyGems cache index rebuilt");
+        }
+        
+        info!("Ruby gems cache updated successfully");
+        Ok(())
+    }
+
+    pub async fn update_general_cache(&mut self) -> SpandxResult<()> {
+        info!("Updating general cache...");
+        
+        // Update only the general cache repository
+        if let Err(e) = self.git_operations.update_repository("cache").await {
+            warn!("Failed to update cache repository: {}", e);
+            return Err(e.into());
+        }
+        
+        // Rebuild cache indices for all package managers
+        if let Some(repo) = self.git_operations.get_repository("cache") {
+            let cache_dir = repo.cache_index_dir();
+            if cache_dir.exists() {
+                // Rebuild for common package managers
+                let package_managers = ["npm", "pypi", "nuget", "maven"];
+                for pm in &package_managers {
+                    if let Err(e) = self.cache.rebuild_index(pm).await {
+                        warn!("Failed to rebuild index for {}: {}", pm, e);
+                    } else {
+                        info!("Rebuilt cache index for {}", pm);
+                    }
+                }
+            }
+        }
+        
+        info!("General cache updated successfully");
+        Ok(())
+    }
+    
+    /// Get status of all Git repositories
+    pub async fn get_repository_status(&self) -> std::collections::HashMap<String, crate::git::operations::RepositoryStatusInfo> {
+        self.git_operations.get_all_status().await
+    }
+    
+    /// Get cache statistics
+    pub async fn get_cache_stats(&mut self, package_manager: &str) -> SpandxResult<CacheStats> {
+        self.cache.stats(package_manager).await
+    }
+    
+    /// Read a file from a Git repository
+    pub async fn read_git_file(&self, repo_name: &str, file_path: &str) -> SpandxResult<String> {
+        self.git_operations.read_file(repo_name, file_path).await.map_err(|e| e.into())
+    }
+    
+    /// Get memory cache (L1) statistics
+    pub fn get_memory_cache_stats(&self) -> MemoryCacheStats {
+        self.cache.memory_cache_stats()
+    }
+    
+    /// Clear the L1 memory cache
+    pub fn clear_memory_cache(&mut self) {
+        self.cache.clear_memory_cache();
+        info!("Cleared L1 memory cache");
+    }
+    
+    /// Preload popular packages into L1 memory cache
+    pub async fn preload_popular_packages(&mut self, package_manager: &str, limit: usize) -> SpandxResult<()> {
+        info!("Preloading {} popular packages for {} into L1 cache", limit, package_manager);
+        self.cache.preload_popular_packages(package_manager, limit).await?;
+        
+        let stats = self.cache.memory_cache_stats();
+        info!("L1 cache now contains {} entries ({:.1}% utilization)", 
+              stats.entries, stats.utilization() * 100.0);
+        Ok(())
+    }
+    
+    /// Get licenses for a package using hierarchical cache
+    pub async fn get_licenses(&mut self, name: &str, version: &str, package_manager: &str) -> SpandxResult<Option<Vec<String>>> {
+        self.cache.get_licenses(name, version, package_manager).await
+    }
+    
+    /// Set licenses for a package in hierarchical cache
+    pub async fn set_licenses(&mut self, name: &str, version: &str, package_manager: &str, licenses: Vec<String>) -> SpandxResult<()> {
+        self.cache.set_licenses(name, version, package_manager, licenses).await
+    }
+    
+    /// Optimize cache performance by warming up frequently accessed packages
+    pub async fn optimize_cache_performance(&mut self) -> SpandxResult<()> {
+        info!("Optimizing cache performance...");
+        
+        // Preload popular packages for major package managers
+        let package_managers = [
+            ("npm", 1000),
+            ("pypi", 500), 
+            ("rubygems", 300),
+            ("maven", 200),
+            ("nuget", 200),
+        ];
+        
+        let mut total_preloaded = 0;
+        for (pm, limit) in &package_managers {
+            match self.preload_popular_packages(pm, *limit).await {
+                Ok(_) => {
+                    let stats = self.get_cache_stats(pm).await?;
+                    total_preloaded += std::cmp::min(*limit, stats.total_entries);
+                    info!("Preloaded up to {} packages for {}", limit, pm);
+                }
+                Err(e) => {
+                    warn!("Failed to preload packages for {}: {}", pm, e);
+                }
+            }
+        }
+        
+        let memory_stats = self.get_memory_cache_stats();
+        info!("Cache optimization complete. Preloaded {} packages total. L1 cache: {}/{} entries ({:.1}% utilization)", 
+              total_preloaded, memory_stats.entries, memory_stats.max_entries, memory_stats.utilization() * 100.0);
+        
+        Ok(())
+    }
+    
+    /// Get comprehensive cache statistics for all levels
+    pub async fn get_comprehensive_stats(&mut self) -> SpandxResult<ComprehensiveCacheStats> {
+        let memory_stats = self.get_memory_cache_stats();
+        
+        // Get disk cache stats for major package managers
+        let mut disk_stats = std::collections::HashMap::new();
+        let package_managers = ["npm", "pypi", "rubygems", "maven", "nuget"];
+        
+        for pm in &package_managers {
+            if let Ok(stats) = self.get_cache_stats(pm).await {
+                disk_stats.insert(pm.to_string(), stats);
+            }
+        }
+        
+        Ok(ComprehensiveCacheStats {
+            memory_cache: memory_stats,
+            disk_cache: disk_stats,
+        })
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct ComprehensiveCacheStats {
+    pub memory_cache: MemoryCacheStats,
+    pub disk_cache: std::collections::HashMap<String, CacheStats>,
+}
+
+impl ComprehensiveCacheStats {
+    pub fn total_disk_entries(&self) -> usize {
+        self.disk_cache.values().map(|stats| stats.total_entries).sum()
+    }
+    
+    pub fn total_disk_buckets(&self) -> usize {
+        self.disk_cache.values().map(|stats| stats.total_buckets).sum()
+    }
+    
+    pub fn cache_efficiency_report(&self) -> String {
+        let mut report = String::new();
+        
+        report.push_str(&format!("L1 Memory Cache: {}/{} entries ({:.1}% utilization)\n",
+            self.memory_cache.entries, 
+            self.memory_cache.max_entries,
+            self.memory_cache.utilization() * 100.0));
+        
+        report.push_str(&format!("L2 Disk Cache: {} total entries across {} package managers\n",
+            self.total_disk_entries(),
+            self.disk_cache.len()));
+        
+        for (pm, stats) in &self.disk_cache {
+            report.push_str(&format!("  {}: {} entries in {} buckets (avg {:.1} per bucket)\n",
+                pm, 
+                stats.total_entries, 
+                stats.total_buckets,
+                stats.avg_entries_per_bucket()));
+        }
+        
+        report
+    }
+}
\ No newline at end of file
src/cache/mod.rs
@@ -0,0 +1,13 @@
+pub mod manager;
+pub mod index;
+pub mod storage;
+pub mod data_file;
+pub mod index_file;
+pub mod cache;
+
+pub use manager::{CacheManager, ComprehensiveCacheStats};
+pub use index::IndexBuilder;
+pub use storage::*;
+pub use data_file::DataFile;
+pub use index_file::IndexFile;
+pub use cache::{Cache, CacheKey, CacheStats, MemoryCacheStats};
\ No newline at end of file
src/cache/storage.rs
@@ -0,0 +1,14 @@
+// Placeholder for cache storage implementation
+pub struct CacheStorage;
+
+impl CacheStorage {
+    pub fn new() -> Self {
+        Self
+    }
+}
+
+impl Default for CacheStorage {
+    fn default() -> Self {
+        Self::new()
+    }
+}
\ No newline at end of file
src/cli/commands/build.rs
@@ -0,0 +1,148 @@
+use anyhow::Result;
+use camino::Utf8PathBuf;
+use tracing::{info, warn};
+
+use crate::cache::{CacheManager, IndexBuilder};
+
+pub struct BuildCommand {
+    pub directory: Utf8PathBuf,
+    pub index: String,
+}
+
+impl BuildCommand {
+    pub fn new(directory: Utf8PathBuf, index: String) -> Self {
+        Self { directory, index }
+    }
+
+    pub async fn execute(&self) -> Result<()> {
+        info!("Building package index in: {}", self.directory);
+        info!("Index type: {}", self.index);
+
+        // Ensure directory exists
+        if !self.directory.exists() {
+            tokio::fs::create_dir_all(&self.directory).await?;
+        }
+
+        let cache_manager = CacheManager::new().await?;
+        let index_builder = IndexBuilder::new(&self.directory);
+
+        match self.index.as_str() {
+            "all" => {
+                info!("Building all indices...");
+                self.build_all_indices(&index_builder, &cache_manager).await?;
+            }
+            "rubygems" | "ruby" => {
+                info!("Building Ruby gems index...");
+                index_builder.build_rubygems_index(&cache_manager).await?;
+            }
+            "npm" | "javascript" | "js" => {
+                info!("Building NPM index...");
+                index_builder.build_npm_index(&cache_manager).await?;
+            }
+            "pypi" | "python" => {
+                info!("Building PyPI index...");
+                index_builder.build_pypi_index(&cache_manager).await?;
+            }
+            "nuget" | "dotnet" => {
+                info!("Building NuGet index...");
+                index_builder.build_nuget_index(&cache_manager).await?;
+            }
+            "maven" | "java" => {
+                info!("Building Maven index...");
+                index_builder.build_maven_index(&cache_manager).await?;
+            }
+            "packagist" | "php" => {
+                info!("Building Packagist index...");
+                index_builder.build_packagist_index(&cache_manager).await?;
+            }
+            "spdx" => {
+                info!("Building SPDX license index...");
+                index_builder.build_spdx_index(&cache_manager).await?;
+            }
+            unknown => {
+                return Err(anyhow::anyhow!("Unknown index type: {}", unknown));
+            }
+        }
+
+        info!("Index building complete");
+        Ok(())
+    }
+
+    async fn build_all_indices(
+        &self,
+        index_builder: &IndexBuilder<'_>,
+        cache_manager: &CacheManager,
+    ) -> Result<()> {
+        let indices = [
+            ("SPDX", "spdx"),
+            ("Ruby gems", "rubygems"),
+            ("NPM", "npm"),
+            ("PyPI", "pypi"),
+            ("NuGet", "nuget"),
+            ("Maven", "maven"),
+            ("Packagist", "packagist"),
+        ];
+
+        for (name, index_type) in &indices {
+            info!("Building {} index...", name);
+            
+            let result = match *index_type {
+                "spdx" => index_builder.build_spdx_index(cache_manager).await,
+                "rubygems" => index_builder.build_rubygems_index(cache_manager).await,
+                "npm" => index_builder.build_npm_index(cache_manager).await,
+                "pypi" => index_builder.build_pypi_index(cache_manager).await,
+                "nuget" => index_builder.build_nuget_index(cache_manager).await,
+                "maven" => index_builder.build_maven_index(cache_manager).await,
+                "packagist" => index_builder.build_packagist_index(cache_manager).await,
+                _ => unreachable!(),
+            };
+
+            if let Err(e) = result {
+                warn!("Failed to build {} index: {}", name, e);
+            } else {
+                info!("Successfully built {} index", name);
+            }
+        }
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use tempfile::TempDir;
+
+    #[test]
+    fn test_build_command_creation() {
+        let cmd = BuildCommand::new(".index".into(), "all".to_string());
+        assert_eq!(cmd.directory.as_str(), ".index");
+        assert_eq!(cmd.index, "all");
+    }
+
+    #[tokio::test]
+    async fn test_build_command_unknown_index() {
+        let temp_dir = TempDir::new().unwrap();
+        let temp_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf()).unwrap();
+        
+        let cmd = BuildCommand::new(temp_path, "unknown".to_string());
+        let result = cmd.execute().await;
+        
+        assert!(result.is_err());
+        assert!(result.unwrap_err().to_string().contains("Unknown index type"));
+    }
+
+    #[test]
+    fn test_valid_index_types() {
+        let valid_types = [
+            "all", "rubygems", "ruby", "npm", "javascript", "js",
+            "pypi", "python", "nuget", "dotnet", "maven", "java",
+            "packagist", "php", "spdx"
+        ];
+
+        for index_type in &valid_types {
+            let cmd = BuildCommand::new(".index".into(), index_type.to_string());
+            assert_eq!(cmd.index, *index_type);
+        }
+    }
+}
\ No newline at end of file
src/cli/commands/mod.rs
@@ -0,0 +1,9 @@
+pub mod scan;
+pub mod pull;
+pub mod build;
+pub mod version;
+
+pub use scan::ScanCommand;
+pub use pull::PullCommand;
+pub use build::BuildCommand;
+pub use version::VersionCommand;
\ No newline at end of file
src/cli/commands/pull.rs
@@ -0,0 +1,91 @@
+use anyhow::Result;
+use tracing::{info, warn};
+
+use crate::cache::CacheManager;
+
+pub struct PullCommand;
+
+impl PullCommand {
+    pub fn new() -> Self {
+        Self
+    }
+
+    pub async fn execute(&self) -> Result<()> {
+        info!("Pulling latest offline cache...");
+
+        let mut cache_manager = CacheManager::new().await?;
+        
+        // Update all repositories and rebuild cache indices
+        match cache_manager.update_all().await {
+            Ok(_) => {
+                info!("All caches updated successfully");
+            }
+            Err(e) => {
+                warn!("Cache update completed with some errors: {}", e);
+                
+                // Try individual updates as fallback
+                info!("Attempting individual repository updates...");
+                
+                // Pull SPDX license data
+                if let Err(e) = cache_manager.update_spdx_cache().await {
+                    warn!("Failed to update SPDX cache: {}", e);
+                }
+
+                // Pull Ruby gems cache
+                if let Err(e) = cache_manager.update_rubygems_cache().await {
+                    warn!("Failed to update Ruby gems cache: {}", e);
+                }
+
+                // Pull general package cache
+                if let Err(e) = cache_manager.update_general_cache().await {
+                    warn!("Failed to update general cache: {}", e);
+                }
+            }
+        }
+        
+        // Display repository status
+        let status = cache_manager.get_repository_status().await;
+        info!("Repository status:");
+        for (name, info) in status {
+            match info.status {
+                crate::git::repository::RepositoryStatus::Clean { commit_hash, .. } => {
+                    info!("  {}: โœ“ up-to-date ({})", name, &commit_hash[..8]);
+                }
+                crate::git::repository::RepositoryStatus::Dirty => {
+                    warn!("  {}: โš  has uncommitted changes", name);
+                }
+                crate::git::repository::RepositoryStatus::NotCloned => {
+                    warn!("  {}: โœ— not cloned", name);
+                }
+            }
+        }
+
+        info!("Cache update complete");
+        Ok(())
+    }
+}
+
+impl Default for PullCommand {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[tokio::test]
+    async fn test_pull_command_creation() {
+        let cmd = PullCommand::new();
+        // Just ensure we can create the command
+        assert!(true);
+    }
+
+    #[test]
+    fn test_pull_command_default() {
+        let cmd = PullCommand::default();
+        // Just ensure we can create the command with default
+        assert!(true);
+    }
+}
\ No newline at end of file
src/cli/commands/scan.rs
@@ -0,0 +1,247 @@
+use crate::error::{SpandxError, SpandxResult};
+use camino::{Utf8Path, Utf8PathBuf};
+use indicatif::{ProgressBar, ProgressStyle};
+use tracing::{debug, info, warn};
+
+use crate::cli::args::OutputFormat;
+use crate::core::{DependencyCollection, ParserRegistry};
+use crate::formatters::FormatterRegistry;
+use crate::parsers::ruby::GemfileLockParser;
+
+pub struct ScanCommand {
+    pub path: Utf8PathBuf,
+    pub recursive: bool,
+    pub airgap: bool,
+    pub format: OutputFormat,
+    pub pull: bool,
+}
+
+impl ScanCommand {
+    pub fn new(
+        path: Utf8PathBuf,
+        recursive: bool,
+        airgap: bool,
+        format: OutputFormat,
+        pull: bool,
+    ) -> Self {
+        Self {
+            path,
+            recursive,
+            airgap,
+            format,
+            pull,
+        }
+    }
+
+    pub async fn execute(&self) -> SpandxResult<()> {
+        info!("Starting scan of: {}", self.path);
+
+        // Set airgap mode globally
+        crate::set_airgap_mode(self.airgap);
+
+        // Pull cache if requested
+        if self.pull {
+            info!("Pulling latest cache...");
+            let pull_command = super::PullCommand::new();
+            if let Err(e) = pull_command.execute().await {
+                warn!("Failed to pull cache: {}", e);
+            }
+        }
+
+        // Initialize parser registry
+        let mut parser_registry = ParserRegistry::new();
+        self.register_parsers(&mut parser_registry);
+
+        // Find files to scan
+        let files = self.find_scannable_files(&parser_registry)?;
+        
+        if files.is_empty() {
+            warn!("No scannable files found");
+            return Ok(());
+        }
+
+        info!("Found {} files to scan", files.len());
+
+        // Scan files with progress bar
+        let dependencies = self.scan_files(&parser_registry, files).await?;
+
+        // Format and output results
+        self.output_results(dependencies).await?;
+
+        Ok(())
+    }
+
+    fn register_parsers(&self, registry: &mut ParserRegistry) {
+        // Register Ruby parser
+        registry.register(GemfileLockParser::new());
+        
+        // Note: These will be implemented in separate modules
+        // registry.register(JavaScriptParser::new());
+        // registry.register(PythonParser::new());
+        // registry.register(DotnetParser::new());
+        // registry.register(JavaParser::new());
+        // registry.register(PhpParser::new());
+        // registry.register(TerraformParser::new());
+        // registry.register(OsParser::new());
+        
+        debug!("Registered {} parsers", registry.parsers().len());
+    }
+
+    fn find_scannable_files(&self, registry: &ParserRegistry) -> SpandxResult<Vec<Utf8PathBuf>> {
+        let mut files = Vec::new();
+
+        if self.path.is_file() {
+            if registry.find_parser(&self.path).is_some() {
+                files.push(self.path.clone());
+            }
+        } else if self.path.is_dir() {
+            files.extend(self.find_files_in_directory(&self.path, registry)?);
+        } else {
+            return Err(SpandxError::FileNotFound {
+                path: self.path.to_string()
+            });
+        }
+
+        Ok(files)
+    }
+
+    fn find_files_in_directory(
+        &self,
+        dir: &Utf8Path,
+        registry: &ParserRegistry,
+    ) -> SpandxResult<Vec<Utf8PathBuf>> {
+        use walkdir::WalkDir;
+        
+        let mut files = Vec::new();
+        let walker = if self.recursive {
+            WalkDir::new(dir)
+        } else {
+            WalkDir::new(dir).max_depth(1)
+        };
+
+        for entry in walker {
+            let entry = entry?;
+            let path = Utf8PathBuf::try_from(entry.path().to_path_buf())?;
+            
+            if path.is_file() && registry.find_parser(&path).is_some() {
+                files.push(path);
+            }
+        }
+
+        Ok(files)
+    }
+
+    async fn scan_files(
+        &self,
+        registry: &ParserRegistry,
+        files: Vec<Utf8PathBuf>,
+    ) -> SpandxResult<DependencyCollection> {
+        let progress_bar = ProgressBar::new(files.len() as u64);
+        progress_bar.set_style(
+            ProgressStyle::default_bar()
+                .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}")?
+                .progress_chars("#>-"),
+        );
+
+        let mut all_dependencies = DependencyCollection::new();
+
+        for file in files {
+            progress_bar.set_message(format!("Scanning {}", file.file_name().unwrap_or("")));
+            
+            match registry.parse_file(&file).await {
+                Ok(dependencies) => {
+                    info!("Found {} dependencies in {}", dependencies.len(), file);
+                    
+                    // Set location for all dependencies
+                    for dep in dependencies.iter().cloned() {
+                        all_dependencies.add(dep.with_location(file.clone()));
+                    }
+                }
+                Err(e) => {
+                    warn!("Failed to parse {}: {}", file, e);
+                }
+            }
+
+            progress_bar.inc(1);
+        }
+
+        progress_bar.finish_with_message("Scan complete");
+        
+        // Sort dependencies by name for consistent output
+        all_dependencies.sort_by_name();
+
+        Ok(all_dependencies)
+    }
+
+    async fn output_results(&self, dependencies: DependencyCollection) -> SpandxResult<()> {
+        let mut formatter_registry = FormatterRegistry::new();
+        formatter_registry.register_all();
+
+        let formatter = formatter_registry
+            .get_formatter(&self.format.to_string())
+            .ok_or_else(|| SpandxError::InvalidArguments {
+                message: format!("Unknown output format: {}", self.format)
+            })?;
+
+        formatter.format(&dependencies).await?;
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use tempfile::TempDir;
+    use std::fs;
+
+    #[tokio::test]
+    async fn test_scan_command_creation() {
+        let cmd = ScanCommand::new(
+            "test.lock".into(),
+            true,
+            false,
+            OutputFormat::Json,
+            false,
+        );
+
+        assert_eq!(cmd.path.as_str(), "test.lock");
+        assert!(cmd.recursive);
+        assert!(!cmd.airgap);
+        assert!(matches!(cmd.format, OutputFormat::Json));
+        assert!(!cmd.pull);
+    }
+
+    #[tokio::test]
+    async fn test_find_scannable_files_empty_directory() {
+        let temp_dir = TempDir::new().unwrap();
+        let temp_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf()).unwrap();
+        
+        let cmd = ScanCommand::new(
+            temp_path,
+            false,
+            false,
+            OutputFormat::Table,
+            false,
+        );
+
+        let registry = ParserRegistry::new();
+        let files = cmd.find_scannable_files(&registry).unwrap();
+        assert!(files.is_empty());
+    }
+
+    #[test]
+    fn test_find_files_nonexistent_path() {
+        let cmd = ScanCommand::new(
+            "/nonexistent/path".into(),
+            false,
+            false,
+            OutputFormat::Table,
+            false,
+        );
+
+        let registry = ParserRegistry::new();
+        let result = cmd.find_scannable_files(&registry);
+        assert!(result.is_err());
+    }
+}
\ No newline at end of file
src/cli/commands/version.rs
@@ -0,0 +1,83 @@
+use anyhow::Result;
+
+pub struct VersionCommand;
+
+impl VersionCommand {
+    pub fn new() -> Self {
+        Self
+    }
+
+    pub async fn execute(&self) -> Result<()> {
+        println!("v{}", env!("CARGO_PKG_VERSION"));
+        Ok(())
+    }
+
+    pub fn version_info() -> VersionInfo {
+        VersionInfo {
+            version: env!("CARGO_PKG_VERSION").to_string(),
+            commit: option_env!("GIT_COMMIT").unwrap_or("unknown").to_string(),
+            build_date: option_env!("BUILD_DATE").unwrap_or("unknown").to_string(),
+            target: std::env::var("TARGET").unwrap_or_else(|_| "unknown".to_string()),
+            rust_version: std::env::var("RUST_VERSION").unwrap_or_else(|_| "unknown".to_string()),
+        }
+    }
+
+    pub async fn execute_detailed(&self) -> Result<()> {
+        let info = Self::version_info();
+        println!("spandx {}", info.version);
+        println!("commit: {}", info.commit);
+        println!("build date: {}", info.build_date);
+        println!("target: {}", info.target);
+        println!("rust version: {}", info.rust_version);
+        Ok(())
+    }
+}
+
+impl Default for VersionCommand {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct VersionInfo {
+    pub version: String,
+    pub commit: String,
+    pub build_date: String,
+    pub target: String,
+    pub rust_version: String,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[tokio::test]
+    async fn test_version_command() {
+        let cmd = VersionCommand::new();
+        let result = cmd.execute().await;
+        assert!(result.is_ok());
+    }
+
+    #[tokio::test]
+    async fn test_version_command_detailed() {
+        let cmd = VersionCommand::new();
+        let result = cmd.execute_detailed().await;
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_version_info() {
+        let info = VersionCommand::version_info();
+        assert!(!info.version.is_empty());
+        assert!(!info.target.is_empty());
+        assert!(!info.rust_version.is_empty());
+    }
+
+    #[test]
+    fn test_version_command_default() {
+        let cmd = VersionCommand::default();
+        // Just ensure we can create the command with default
+        assert!(true);
+    }
+}
\ No newline at end of file
src/cli/args.rs
@@ -0,0 +1,177 @@
+use camino::Utf8PathBuf;
+use clap::{Parser, Subcommand, ValueEnum};
+
+#[derive(Parser)]
+#[command(
+    name = "spandx",
+    version = env!("CARGO_PKG_VERSION"),
+    about = "A Rust interface to the SPDX catalogue for dependency license scanning",
+    long_about = None,
+    author = "Can Eldem <eldemcan@gmail.com>, mo khan <mo@mokhan.ca>"
+)]
+pub struct Cli {
+    #[command(subcommand)]
+    pub command: Commands,
+}
+
+#[derive(Subcommand)]
+pub enum Commands {
+    /// Scan a lockfile and list dependencies/licenses
+    Scan {
+        /// Path to the lockfile or directory to scan
+        #[arg(default_value = ".")]
+        path: Utf8PathBuf,
+
+        /// Perform recursive directory scanning
+        #[arg(short = 'R', long = "recursive")]
+        recursive: bool,
+
+        /// Disable network connections (air-gap mode)
+        #[arg(short = 'a', long = "airgap")]
+        airgap: bool,
+
+        /// Path to a logfile
+        #[arg(short = 'l', long = "logfile", default_value = "/dev/null")]
+        logfile: Utf8PathBuf,
+
+        /// Format of report (table, csv, json)
+        #[arg(short = 'f', long = "format", default_value = "table")]
+        format: OutputFormat,
+
+        /// Pull the latest cache before the scan
+        #[arg(short = 'p', long = "pull")]
+        pull: bool,
+
+        /// Load additional modules (for extensibility)
+        #[arg(short = 'r', long = "require")]
+        require: Option<String>,
+    },
+
+    /// Pull the latest offline cache
+    Pull,
+
+    /// Build a package index
+    Build {
+        /// Directory to build index in
+        #[arg(short = 'd', long = "directory", default_value = ".index")]
+        directory: Utf8PathBuf,
+
+        /// Path to a logfile
+        #[arg(short = 'l', long = "logfile", default_value = "/dev/null")]
+        logfile: Utf8PathBuf,
+
+        /// The specific index to build
+        #[arg(short = 'i', long = "index", default_value = "all")]
+        index: String,
+    },
+
+    /// Display version information
+    Version,
+}
+
+#[derive(ValueEnum, Clone, Debug)]
+pub enum OutputFormat {
+    Table,
+    Csv,
+    Json,
+}
+
+impl std::fmt::Display for OutputFormat {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            OutputFormat::Table => write!(f, "table"),
+            OutputFormat::Csv => write!(f, "csv"),
+            OutputFormat::Json => write!(f, "json"),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use clap::Parser;
+
+    #[test]
+    fn test_cli_scan_default() {
+        let cli = Cli::parse_from(&["spandx", "scan"]);
+        
+        if let Commands::Scan { 
+            path, 
+            recursive, 
+            airgap, 
+            format, 
+            pull, 
+            .. 
+        } = cli.command {
+            assert_eq!(path.as_str(), ".");
+            assert!(!recursive);
+            assert!(!airgap);
+            assert!(matches!(format, OutputFormat::Table));
+            assert!(!pull);
+        } else {
+            panic!("Expected scan command");
+        }
+    }
+
+    #[test]
+    fn test_cli_scan_with_options() {
+        let cli = Cli::parse_from(&[
+            "spandx", 
+            "scan", 
+            "Gemfile.lock",
+            "--recursive",
+            "--airgap", 
+            "--format", 
+            "json",
+            "--pull"
+        ]);
+        
+        if let Commands::Scan { 
+            path, 
+            recursive, 
+            airgap, 
+            format, 
+            pull, 
+            .. 
+        } = cli.command {
+            assert_eq!(path.as_str(), "Gemfile.lock");
+            assert!(recursive);
+            assert!(airgap);
+            assert!(matches!(format, OutputFormat::Json));
+            assert!(pull);
+        } else {
+            panic!("Expected scan command");
+        }
+    }
+
+    #[test]
+    fn test_cli_pull() {
+        let cli = Cli::parse_from(&["spandx", "pull"]);
+        assert!(matches!(cli.command, Commands::Pull));
+    }
+
+    #[test]
+    fn test_cli_build() {
+        let cli = Cli::parse_from(&["spandx", "build"]);
+        
+        if let Commands::Build { directory, index, .. } = cli.command {
+            assert_eq!(directory.as_str(), ".index");
+            assert_eq!(index, "all");
+        } else {
+            panic!("Expected build command");
+        }
+    }
+
+    #[test]
+    fn test_cli_version() {
+        let cli = Cli::parse_from(&["spandx", "version"]);
+        assert!(matches!(cli.command, Commands::Version));
+    }
+
+    #[test]
+    fn test_output_format_display() {
+        assert_eq!(format!("{}", OutputFormat::Table), "table");
+        assert_eq!(format!("{}", OutputFormat::Csv), "csv");
+        assert_eq!(format!("{}", OutputFormat::Json), "json");
+    }
+}
\ No newline at end of file
src/cli/mod.rs
@@ -0,0 +1,5 @@
+pub mod args;
+pub mod commands;
+
+pub use args::*;
+pub use commands::*;
\ No newline at end of file
src/core/cache.rs
@@ -0,0 +1,2 @@
+// Placeholder for cache implementation
+// Will be implemented with binary-indexed cache system
\ No newline at end of file
src/core/circuit.rs
@@ -0,0 +1,206 @@
+use std::time::{Duration, Instant};
+
+#[derive(Debug, Clone)]
+pub struct CircuitBreaker {
+    failure_count: u32,
+    failure_threshold: u32,
+    reset_timeout: Duration,
+    last_failure_time: Option<Instant>,
+    state: CircuitBreakerState,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+enum CircuitBreakerState {
+    Closed,
+    Open,
+    HalfOpen,
+}
+
+impl CircuitBreaker {
+    pub fn new() -> Self {
+        Self::with_threshold(5)
+    }
+
+    pub fn with_threshold(failure_threshold: u32) -> Self {
+        Self {
+            failure_count: 0,
+            failure_threshold,
+            reset_timeout: Duration::from_secs(60),
+            last_failure_time: None,
+            state: CircuitBreakerState::Closed,
+        }
+    }
+
+    pub fn with_reset_timeout(mut self, timeout: Duration) -> Self {
+        self.reset_timeout = timeout;
+        self
+    }
+
+    pub fn is_open(&self) -> bool {
+        match self.state {
+            CircuitBreakerState::Open => {
+                if let Some(last_failure) = self.last_failure_time {
+                    Instant::now().duration_since(last_failure) < self.reset_timeout
+                } else {
+                    false
+                }
+            }
+            CircuitBreakerState::HalfOpen => false,
+            CircuitBreakerState::Closed => false,
+        }
+    }
+
+    pub fn is_closed(&self) -> bool {
+        matches!(self.state, CircuitBreakerState::Closed)
+    }
+
+    pub fn is_half_open(&self) -> bool {
+        matches!(self.state, CircuitBreakerState::HalfOpen)
+    }
+
+    pub fn record_success(&mut self) {
+        self.failure_count = 0;
+        self.last_failure_time = None;
+        self.state = CircuitBreakerState::Closed;
+    }
+
+    pub fn record_failure(&mut self) {
+        self.failure_count += 1;
+        self.last_failure_time = Some(Instant::now());
+
+        if self.failure_count >= self.failure_threshold {
+            self.state = CircuitBreakerState::Open;
+        }
+    }
+
+    pub fn attempt_reset(&mut self) -> bool {
+        if self.state == CircuitBreakerState::Open {
+            if let Some(last_failure) = self.last_failure_time {
+                if Instant::now().duration_since(last_failure) >= self.reset_timeout {
+                    self.state = CircuitBreakerState::HalfOpen;
+                    return true;
+                }
+            }
+        }
+        false
+    }
+
+    pub fn reset(&mut self) {
+        self.failure_count = 0;
+        self.last_failure_time = None;
+        self.state = CircuitBreakerState::Closed;
+    }
+
+    pub fn failure_count(&self) -> u32 {
+        self.failure_count
+    }
+
+    pub fn state_name(&self) -> &'static str {
+        match self.state {
+            CircuitBreakerState::Closed => "closed",
+            CircuitBreakerState::Open => "open",
+            CircuitBreakerState::HalfOpen => "half-open",
+        }
+    }
+}
+
+impl Default for CircuitBreaker {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::thread;
+
+    #[test]
+    fn test_circuit_breaker_closed_state() {
+        let cb = CircuitBreaker::new();
+        assert!(cb.is_closed());
+        assert!(!cb.is_open());
+        assert!(!cb.is_half_open());
+        assert_eq!(cb.failure_count(), 0);
+    }
+
+    #[test]
+    fn test_circuit_breaker_failure_threshold() {
+        let mut cb = CircuitBreaker::with_threshold(3);
+        
+        // Record 2 failures - should stay closed
+        cb.record_failure();
+        cb.record_failure();
+        assert!(cb.is_closed());
+        assert_eq!(cb.failure_count(), 2);
+        
+        // Record 3rd failure - should open
+        cb.record_failure();
+        assert!(cb.is_open());
+        assert_eq!(cb.failure_count(), 3);
+    }
+
+    #[test]
+    fn test_circuit_breaker_success_resets() {
+        let mut cb = CircuitBreaker::with_threshold(2);
+        
+        // Record failures
+        cb.record_failure();
+        cb.record_failure();
+        assert!(cb.is_open());
+        
+        // Record success - should close
+        cb.record_success();
+        assert!(cb.is_closed());
+        assert_eq!(cb.failure_count(), 0);
+    }
+
+    #[test]
+    fn test_circuit_breaker_timeout() {
+        let mut cb = CircuitBreaker::with_threshold(1)
+            .with_reset_timeout(Duration::from_millis(100));
+        
+        // Trigger opening
+        cb.record_failure();
+        assert!(cb.is_open());
+        
+        // Should still be open immediately
+        assert!(cb.is_open());
+        
+        // Wait for timeout
+        thread::sleep(Duration::from_millis(150));
+        
+        // Should allow attempt reset
+        assert!(cb.attempt_reset());
+        assert!(cb.is_half_open());
+    }
+
+    #[test]
+    fn test_circuit_breaker_manual_reset() {
+        let mut cb = CircuitBreaker::with_threshold(1);
+        
+        cb.record_failure();
+        assert!(cb.is_open());
+        
+        cb.reset();
+        assert!(cb.is_closed());
+        assert_eq!(cb.failure_count(), 0);
+    }
+
+    #[test]
+    fn test_circuit_breaker_state_names() {
+        let mut cb = CircuitBreaker::new();
+        
+        assert_eq!(cb.state_name(), "closed");
+        
+        cb.record_failure();
+        cb.record_failure();
+        cb.record_failure();
+        cb.record_failure();
+        cb.record_failure();
+        assert_eq!(cb.state_name(), "open");
+        
+        cb.state = CircuitBreakerState::HalfOpen;
+        assert_eq!(cb.state_name(), "half-open");
+    }
+}
\ No newline at end of file
src/core/content.rs
@@ -0,0 +1,323 @@
+use std::collections::HashSet;
+use regex::Regex;
+
+/// Represents textual content with similarity scoring capabilities
+#[derive(Debug, Clone, PartialEq)]
+pub struct Content {
+    text: String,
+    tokens: HashSet<String>,
+}
+
+impl Content {
+    /// Create a new Content instance with the given text
+    pub fn new(text: String) -> Self {
+        let tokens = Self::tokenize(&Self::canonicalize(&text));
+        Self { text, tokens }
+    }
+
+    /// Get the original text
+    pub fn text(&self) -> &str {
+        &self.text
+    }
+
+    /// Get the tokens
+    pub fn tokens(&self) -> &HashSet<String> {
+        &self.tokens
+    }
+
+    /// Calculate Dice coefficient similarity with another Content instance
+    /// Returns a percentage (0.0 - 100.0)
+    pub fn similarity_score(&self, other: &Content) -> f64 {
+        self.dice_coefficient(other)
+    }
+
+    /// Calculate Dice coefficient between two Content instances
+    /// Formula: 2 * |X โˆฉ Y| / (|X| + |Y|) * 100
+    pub fn dice_coefficient(&self, other: &Content) -> f64 {
+        let overlap = self.tokens.intersection(&other.tokens).count();
+        let total = self.tokens.len() + other.tokens.len();
+        
+        if total == 0 {
+            0.0
+        } else {
+            100.0 * (overlap as f64 * 2.0 / total as f64)
+        }
+    }
+
+    /// Canonicalize text by converting to lowercase
+    fn canonicalize(text: &str) -> String {
+        text.to_lowercase()
+    }
+
+    /// Tokenize text by extracting alphanumeric words and dots
+    /// Matches Ruby regex: /[a-zA-Z\d.]+/
+    fn tokenize(text: &str) -> HashSet<String> {
+        lazy_static::lazy_static! {
+            static ref TOKEN_REGEX: Regex = Regex::new(r"[a-zA-Z\d.]+").unwrap();
+        }
+        
+        TOKEN_REGEX
+            .find_iter(text)
+            .map(|m| m.as_str().to_string())
+            .collect()
+    }
+
+    /// Create Content from a string slice
+    pub fn from_str(text: &str) -> Self {
+        Self::new(text.to_string())
+    }
+
+    /// Check if content is empty (no tokens)
+    pub fn is_empty(&self) -> bool {
+        self.tokens.is_empty()
+    }
+
+    /// Get the number of unique tokens
+    pub fn token_count(&self) -> usize {
+        self.tokens.len()
+    }
+
+    /// Get common tokens with another Content instance
+    pub fn common_tokens(&self, other: &Content) -> HashSet<String> {
+        self.tokens.intersection(&other.tokens).cloned().collect()
+    }
+
+    /// Get union of tokens with another Content instance
+    pub fn union_tokens(&self, other: &Content) -> HashSet<String> {
+        self.tokens.union(&other.tokens).cloned().collect()
+    }
+
+    /// Calculate Jaccard similarity coefficient
+    /// Formula: |X โˆฉ Y| / |X โˆช Y| * 100
+    pub fn jaccard_coefficient(&self, other: &Content) -> f64 {
+        let intersection_size = self.tokens.intersection(&other.tokens).count();
+        let union_size = self.tokens.union(&other.tokens).count();
+        
+        if union_size == 0 {
+            0.0
+        } else {
+            100.0 * (intersection_size as f64 / union_size as f64)
+        }
+    }
+
+    /// Calculate cosine similarity
+    /// Formula: |X โˆฉ Y| / sqrt(|X| * |Y|) * 100
+    pub fn cosine_similarity(&self, other: &Content) -> f64 {
+        let intersection_size = self.tokens.intersection(&other.tokens).count();
+        let magnitude_product = (self.tokens.len() as f64 * other.tokens.len() as f64).sqrt();
+        
+        if magnitude_product == 0.0 {
+            0.0
+        } else {
+            100.0 * (intersection_size as f64 / magnitude_product)
+        }
+    }
+}
+
+impl From<String> for Content {
+    fn from(text: String) -> Self {
+        Self::new(text)
+    }
+}
+
+impl From<&str> for Content {
+    fn from(text: &str) -> Self {
+        Self::new(text.to_string())
+    }
+}
+
+impl std::fmt::Display for Content {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.text)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_content_creation() {
+        let content = Content::new("MIT License".to_string());
+        assert_eq!(content.text(), "MIT License");
+        assert_eq!(content.token_count(), 2);
+        assert!(content.tokens().contains("mit"));
+        assert!(content.tokens().contains("license"));
+    }
+
+    #[test]
+    fn test_tokenization() {
+        let content = Content::new("MIT License v2.0".to_string());
+        let tokens = content.tokens();
+        
+        assert_eq!(tokens.len(), 3); // v2.0 is a single token (includes dots)
+        assert!(tokens.contains("mit"));
+        assert!(tokens.contains("license"));
+        assert!(tokens.contains("v2.0"));
+    }
+
+    #[test]
+    fn test_tokenization_with_dots() {
+        let content = Content::new("Apache-2.0 License v1.2.3".to_string());
+        let tokens = content.tokens();
+        
+        // Should extract: apache, 2.0, license, v1.2.3
+        assert!(tokens.contains("apache"));
+        assert!(tokens.contains("2.0"));
+        assert!(tokens.contains("license"));
+        assert!(tokens.contains("v1.2.3"));
+    }
+
+    #[test]
+    fn test_canonicalization() {
+        let content1 = Content::new("MIT License".to_string());
+        let content2 = Content::new("mit license".to_string());
+        
+        assert_eq!(content1.tokens(), content2.tokens());
+    }
+
+    #[test]
+    fn test_dice_coefficient_identical() {
+        let content1 = Content::new("MIT License".to_string());
+        let content2 = Content::new("MIT License".to_string());
+        
+        assert!((content1.dice_coefficient(&content2) - 100.0).abs() < f64::EPSILON);
+    }
+
+    #[test]
+    fn test_dice_coefficient_no_overlap() {
+        let content1 = Content::new("MIT License".to_string());
+        let content2 = Content::new("Apache BSD".to_string());
+        
+        assert!((content1.dice_coefficient(&content2) - 0.0).abs() < f64::EPSILON);
+    }
+
+    #[test]
+    fn test_dice_coefficient_partial_overlap() {
+        let content1 = Content::new("MIT License".to_string());
+        let content2 = Content::new("MIT BSD".to_string());
+        
+        // Tokens: content1 = {mit, license}, content2 = {mit, bsd}
+        // Overlap: {mit} = 1
+        // Total: 2 + 2 = 4
+        // Dice: 2 * 1 / 4 * 100 = 50.0
+        assert!((content1.dice_coefficient(&content2) - 50.0).abs() < f64::EPSILON);
+    }
+
+    #[test]
+    fn test_dice_coefficient_empty_content() {
+        let content1 = Content::new("".to_string());
+        let content2 = Content::new("MIT License".to_string());
+        
+        assert!((content1.dice_coefficient(&content2) - 0.0).abs() < f64::EPSILON);
+    }
+
+    #[test]
+    fn test_dice_coefficient_both_empty() {
+        let content1 = Content::new("".to_string());
+        let content2 = Content::new("".to_string());
+        
+        assert!((content1.dice_coefficient(&content2) - 0.0).abs() < f64::EPSILON);
+    }
+
+    #[test]
+    fn test_similarity_score() {
+        let content1 = Content::new("MIT License".to_string());
+        let content2 = Content::new("MIT BSD License".to_string());
+        
+        // Tokens: content1 = {mit, license}, content2 = {mit, bsd, license}
+        // Overlap: {mit, license} = 2
+        // Total: 2 + 3 = 5
+        // Dice: 2 * 2 / 5 * 100 = 80.0
+        assert!((content1.similarity_score(&content2) - 80.0).abs() < f64::EPSILON);
+    }
+
+    #[test]
+    fn test_jaccard_coefficient() {
+        let content1 = Content::new("MIT License".to_string());
+        let content2 = Content::new("MIT BSD License".to_string());
+        
+        // Tokens: content1 = {mit, license}, content2 = {mit, bsd, license}
+        // Intersection: {mit, license} = 2
+        // Union: {mit, license, bsd} = 3
+        // Jaccard: 2 / 3 * 100 = 66.67
+        let score = content1.jaccard_coefficient(&content2);
+        assert!((score - 66.66666666666667).abs() < 0.01);
+    }
+
+    #[test]
+    fn test_cosine_similarity() {
+        let content1 = Content::new("MIT License".to_string());
+        let content2 = Content::new("MIT BSD License".to_string());
+        
+        // Tokens: content1 = {mit, license}, content2 = {mit, bsd, license}
+        // Intersection: {mit, license} = 2
+        // Magnitudes: sqrt(2 * 3) = sqrt(6) โ‰ˆ 2.449
+        // Cosine: 2 / 2.449 * 100 โ‰ˆ 81.65
+        let score = content1.cosine_similarity(&content2);
+        assert!((score - 81.64965809277261).abs() < 0.01);
+    }
+
+    #[test]
+    fn test_common_tokens() {
+        let content1 = Content::new("MIT License".to_string());
+        let content2 = Content::new("MIT BSD License".to_string());
+        
+        let common = content1.common_tokens(&content2);
+        assert_eq!(common.len(), 2);
+        assert!(common.contains("mit"));
+        assert!(common.contains("license"));
+    }
+
+    #[test]
+    fn test_union_tokens() {
+        let content1 = Content::new("MIT License".to_string());
+        let content2 = Content::new("MIT BSD".to_string());
+        
+        let union = content1.union_tokens(&content2);
+        assert_eq!(union.len(), 3);
+        assert!(union.contains("mit"));
+        assert!(union.contains("license"));
+        assert!(union.contains("bsd"));
+    }
+
+    #[test]
+    fn test_from_conversions() {
+        let content1 = Content::from("MIT License".to_string());
+        let content2 = Content::from("MIT License");
+        
+        assert_eq!(content1.text(), content2.text());
+        assert_eq!(content1.tokens(), content2.tokens());
+    }
+
+    #[test]
+    fn test_display() {
+        let content = Content::new("MIT License".to_string());
+        assert_eq!(format!("{}", content), "MIT License");
+    }
+
+    #[test]
+    fn test_is_empty() {
+        let empty_content = Content::new("".to_string());
+        let non_empty_content = Content::new("MIT".to_string());
+        
+        assert!(empty_content.is_empty());
+        assert!(!non_empty_content.is_empty());
+    }
+
+    #[test]
+    fn test_special_characters() {
+        let content = Content::new("MIT/Apache-2.0 (dual license)".to_string());
+        let tokens = content.tokens();
+        
+        // Should extract alphanumeric words and dots, ignoring other punctuation
+        assert!(tokens.contains("mit"));
+        assert!(tokens.contains("apache"));
+        assert!(tokens.contains("2.0"));
+        assert!(tokens.contains("dual"));
+        assert!(tokens.contains("license"));
+        assert!(!tokens.contains("/"));
+        assert!(!tokens.contains("("));
+        assert!(!tokens.contains(")"));
+    }
+}
\ No newline at end of file
src/core/dependency.rs
@@ -0,0 +1,200 @@
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::fmt;
+use camino::Utf8PathBuf;
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Dependency {
+    pub name: String,
+    pub version: String,
+    pub licenses: Vec<String>,
+    pub location: Utf8PathBuf,
+    pub source: Option<String>,
+    pub metadata: HashMap<String, String>,
+}
+
+impl Dependency {
+    pub fn new(name: String, version: String) -> Self {
+        Self {
+            name,
+            version,
+            licenses: Vec::new(),
+            location: Utf8PathBuf::new(),
+            source: None,
+            metadata: HashMap::new(),
+        }
+    }
+
+    pub fn with_location(mut self, location: Utf8PathBuf) -> Self {
+        self.location = location;
+        self
+    }
+
+    pub fn with_source(mut self, source: String) -> Self {
+        self.source = Some(source);
+        self
+    }
+
+    pub fn with_license(mut self, license: String) -> Self {
+        self.licenses.push(license);
+        self
+    }
+
+    pub fn with_licenses(mut self, licenses: Vec<String>) -> Self {
+        self.licenses = licenses;
+        self
+    }
+
+    pub fn add_metadata(mut self, key: String, value: String) -> Self {
+        self.metadata.insert(key, value);
+        self
+    }
+
+    pub fn id(&self) -> String {
+        format!("{}:{}", self.name, self.version)
+    }
+
+    pub fn has_licenses(&self) -> bool {
+        !self.licenses.is_empty()
+    }
+
+    pub fn license_display(&self) -> String {
+        if self.licenses.is_empty() {
+            "Unknown".to_string()
+        } else {
+            self.licenses.join(", ")
+        }
+    }
+}
+
+impl fmt::Display for Dependency {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} ({})", self.name, self.version)
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DependencyCollection {
+    dependencies: Vec<Dependency>,
+}
+
+impl DependencyCollection {
+    pub fn new() -> Self {
+        Self {
+            dependencies: Vec::new(),
+        }
+    }
+
+    pub fn add(&mut self, dependency: Dependency) {
+        self.dependencies.push(dependency);
+    }
+
+    pub fn extend(&mut self, other: DependencyCollection) {
+        self.dependencies.extend(other.dependencies);
+    }
+
+    pub fn iter(&self) -> impl Iterator<Item = &Dependency> {
+        self.dependencies.iter()
+    }
+
+    pub fn into_iter(self) -> impl Iterator<Item = Dependency> {
+        self.dependencies.into_iter()
+    }
+
+    pub fn len(&self) -> usize {
+        self.dependencies.len()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.dependencies.is_empty()
+    }
+
+    pub fn sort_by_name(&mut self) {
+        self.dependencies.sort_by(|a, b| a.name.cmp(&b.name));
+    }
+
+    pub fn filter_by_location(&self, location: &Utf8PathBuf) -> DependencyCollection {
+        let filtered: Vec<Dependency> = self
+            .dependencies
+            .iter()
+            .filter(|dep| dep.location == *location)
+            .cloned()
+            .collect();
+        
+        DependencyCollection {
+            dependencies: filtered,
+        }
+    }
+
+    pub fn unique_licenses(&self) -> Vec<String> {
+        let mut licenses = std::collections::HashSet::new();
+        for dep in &self.dependencies {
+            for license in &dep.licenses {
+                licenses.insert(license.clone());
+            }
+        }
+        let mut unique_licenses: Vec<String> = licenses.into_iter().collect();
+        unique_licenses.sort();
+        unique_licenses
+    }
+}
+
+impl Default for DependencyCollection {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl From<Vec<Dependency>> for DependencyCollection {
+    fn from(dependencies: Vec<Dependency>) -> Self {
+        Self { dependencies }
+    }
+}
+
+impl IntoIterator for DependencyCollection {
+    type Item = Dependency;
+    type IntoIter = std::vec::IntoIter<Self::Item>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.dependencies.into_iter()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_dependency_creation() {
+        let dep = Dependency::new("test".to_string(), "1.0.0".to_string());
+        assert_eq!(dep.name, "test");
+        assert_eq!(dep.version, "1.0.0");
+        assert!(dep.licenses.is_empty());
+    }
+
+    #[test]
+    fn test_dependency_builder() {
+        let dep = Dependency::new("test".to_string(), "1.0.0".to_string())
+            .with_license("MIT".to_string())
+            .with_source("rubygems".to_string());
+        
+        assert_eq!(dep.licenses, vec!["MIT"]);
+        assert_eq!(dep.source, Some("rubygems".to_string()));
+    }
+
+    #[test]
+    fn test_dependency_id() {
+        let dep = Dependency::new("test".to_string(), "1.0.0".to_string());
+        assert_eq!(dep.id(), "test:1.0.0");
+    }
+
+    #[test]
+    fn test_dependency_collection() {
+        let mut collection = DependencyCollection::new();
+        let dep = Dependency::new("test".to_string(), "1.0.0".to_string());
+        
+        collection.add(dep);
+        assert_eq!(collection.len(), 1);
+        assert!(!collection.is_empty());
+    }
+}
\ No newline at end of file
src/core/git.rs
@@ -0,0 +1,2 @@
+// Placeholder for git operations
+// Will be implemented with git2 for cache management
\ No newline at end of file
src/core/guess.rs
@@ -0,0 +1,467 @@
+use anyhow::Result;
+use tracing::debug;
+
+use crate::core::{Content, Score};
+use crate::spdx::{Catalogue, ExpressionParser};
+
+/// License detection engine with multiple matching strategies
+#[derive(Debug)]
+pub struct Guess {
+    catalogue: Catalogue,
+    name_similarity_threshold: f64,
+    body_similarity_threshold: f64,
+}
+
+/// Input types for license detection
+#[derive(Debug, Clone)]
+pub enum GuessInput {
+    String(String),
+    Array(Vec<String>),
+    Hash(std::collections::HashMap<String, String>),
+}
+
+impl From<String> for GuessInput {
+    fn from(s: String) -> Self {
+        GuessInput::String(s)
+    }
+}
+
+impl From<&str> for GuessInput {
+    fn from(s: &str) -> Self {
+        GuessInput::String(s.to_string())
+    }
+}
+
+impl From<Vec<String>> for GuessInput {
+    fn from(v: Vec<String>) -> Self {
+        GuessInput::Array(v)
+    }
+}
+
+impl From<Vec<&str>> for GuessInput {
+    fn from(v: Vec<&str>) -> Self {
+        GuessInput::Array(v.into_iter().map(|s| s.to_string()).collect())
+    }
+}
+
+impl From<std::collections::HashMap<String, String>> for GuessInput {
+    fn from(h: std::collections::HashMap<String, String>) -> Self {
+        GuessInput::Hash(h)
+    }
+}
+
+impl Guess {
+    /// Create a new Guess instance with default thresholds
+    pub fn new(catalogue: Catalogue) -> Self {
+        Self {
+            catalogue,
+            name_similarity_threshold: 85.0, // 85% threshold for name matching (from Ruby)
+            body_similarity_threshold: 89.0, // 89% threshold for body matching (from Ruby)
+        }
+    }
+
+    /// Create a new Guess instance with custom thresholds
+    pub fn with_thresholds(catalogue: Catalogue, name_threshold: f64, body_threshold: f64) -> Self {
+        Self {
+            catalogue,
+            name_similarity_threshold: name_threshold,
+            body_similarity_threshold: body_threshold,
+        }
+    }
+
+    /// Main license detection method
+    pub async fn detect_license(&self, input: GuessInput) -> Result<String> {
+        match input {
+            GuessInput::String(content) => self.detect_from_string(&content).await,
+            GuessInput::Array(licenses) => self.detect_from_array(&licenses).await,
+            GuessInput::Hash(metadata) => self.detect_from_hash(&metadata).await,
+        }
+    }
+
+    /// Detect license from a single string (license name or content)
+    async fn detect_from_string(&self, content: &str) -> Result<String> {
+        let content = content.trim();
+        
+        if content.is_empty() {
+            return Ok("unknown".to_string());
+        }
+
+        debug!("Detecting license from string: {}", &content[..std::cmp::min(100, content.len())]);
+
+        // Strategy 1: Try exact match in catalogue
+        if let Some(license) = self.catalogue.get(content) {
+            debug!("Found exact match: {}", license.id);
+            return Ok(license.id.clone());
+        }
+
+        // Strategy 2: Try parsing as SPDX expression
+        let parser = ExpressionParser::new();
+        if let Ok(expression) = parser.parse(content) {
+            debug!("Parsed as SPDX expression: {:?}", expression);
+            return Ok(content.to_string()); // Return original expression string
+        }
+
+        // Strategy 3: Try name similarity matching
+        if let Some(license_id) = self.find_similar_name(content).await? {
+            debug!("Found similar name: {}", license_id);
+            return Ok(license_id);
+        }
+
+        // Strategy 4: Try body/content similarity matching
+        if content.len() > 50 { // Only try body matching for longer content
+            if let Some(license_id) = self.find_similar_body(content).await? {
+                debug!("Found similar body: {}", license_id);
+                return Ok(license_id);
+            }
+        }
+
+        debug!("No match found, returning unknown");
+        Ok("unknown".to_string())
+    }
+
+    /// Detect license from an array of license strings
+    async fn detect_from_array(&self, licenses: &[String]) -> Result<String> {
+        if licenses.is_empty() {
+            return Ok("unknown".to_string());
+        }
+
+        debug!("Detecting license from array of {} items", licenses.len());
+
+        // Try each license string until we find a match
+        for license_str in licenses {
+            let result = self.detect_from_string(license_str).await?;
+            if result != "unknown" {
+                return Ok(result);
+            }
+        }
+
+        // If no individual matches, try combining them as an expression
+        let combined = licenses.join(" AND ");
+        let parser = ExpressionParser::new();
+        if let Ok(_expression) = parser.parse(&combined) {
+            debug!("Parsed combined array as SPDX expression: {}", combined);
+            return Ok(combined);
+        }
+
+        Ok("unknown".to_string())
+    }
+
+    /// Detect license from a hash/map of metadata
+    async fn detect_from_hash(&self, metadata: &std::collections::HashMap<String, String>) -> Result<String> {
+        debug!("Detecting license from hash with {} keys", metadata.len());
+
+        // Look for common license fields
+        let license_fields = [
+            "license", "License", "LICENSE",
+            "license_id", "licenseId", "license-id",
+            "spdx_id", "spdxId", "spdx-id",
+            "name", "title",
+        ];
+
+        for field in &license_fields {
+            if let Some(value) = metadata.get(*field) {
+                let result = self.detect_from_string(value).await?;
+                if result != "unknown" {
+                    debug!("Found license in field '{}': {}", field, result);
+                    return Ok(result);
+                }
+            }
+        }
+
+        // Try license text/body fields
+        let body_fields = [
+            "text", "body", "content", "license_text", "licenseText",
+            "full_text", "fullText", "description",
+        ];
+
+        for field in &body_fields {
+            if let Some(value) = metadata.get(*field) {
+                if value.len() > 100 { // Only try body matching for substantial content
+                    let result = self.detect_from_string(value).await?;
+                    if result != "unknown" {
+                        debug!("Found license in body field '{}': {}", field, result);
+                        return Ok(result);
+                    }
+                }
+            }
+        }
+
+        Ok("unknown".to_string())
+    }
+
+    /// Find similar license by name using Dice coefficient
+    async fn find_similar_name(&self, name: &str) -> Result<Option<String>> {
+        let input_content = Content::from(name);
+        let mut best_score = Score::zero();
+
+        for license in self.catalogue.licenses() {
+            // Try license ID
+            let id_content = Content::from(license.id.as_str());
+            let id_score = input_content.similarity_score(&id_content);
+            
+            if id_score >= self.name_similarity_threshold {
+                best_score.update_if_better(license.id.clone(), id_score);
+            }
+
+            // Try license name
+            let name_content = Content::from(license.name.as_str());
+            let name_score = input_content.similarity_score(&name_content);
+            
+            if name_score >= self.name_similarity_threshold {
+                best_score.update_if_better(license.id.clone(), name_score);
+            }
+        }
+
+        if best_score.meets_threshold(self.name_similarity_threshold) {
+            debug!("Best name similarity: {}", best_score);
+            Ok(Some(best_score.license_id().to_string()))
+        } else {
+            Ok(None)
+        }
+    }
+
+    /// Find similar license by body/content using Dice coefficient
+    async fn find_similar_body(&self, content: &str) -> Result<Option<String>> {
+        let input_content = Content::from(content);
+        let mut best_score = Score::zero();
+
+        for license in self.catalogue.licenses() {
+            // Skip deprecated licenses for body matching
+            if license.is_deprecated() {
+                continue;
+            }
+
+            // Try to get license text
+            if let Some(license_text) = self.get_license_text(&license.id).await? {
+                let license_content = Content::from(license_text.as_str());
+                let score = input_content.similarity_score(&license_content);
+                
+                if score >= self.body_similarity_threshold {
+                    best_score.update_if_better(license.id.clone(), score);
+                }
+            }
+        }
+
+        if best_score.meets_threshold(self.body_similarity_threshold) {
+            debug!("Best body similarity: {}", best_score);
+            Ok(Some(best_score.license_id().to_string()))
+        } else {
+            Ok(None)
+        }
+    }
+
+    /// Get license text from SPDX repository or other sources
+    async fn get_license_text(&self, license_id: &str) -> Result<Option<String>> {
+        // Try to load from SPDX license text
+        // This would integrate with the Git operations to load from the SPDX repository
+        // For now, return None to avoid blocking the implementation
+        
+        // TODO: Integrate with GitOperations to load license text from spdx repository
+        // Something like:
+        // let git_ops = GitOperations::new(...);
+        // let license_text = git_ops.read_file("spdx", &format!("text/{}.txt", license_id)).await?;
+        
+        debug!("License text loading not yet implemented for: {}", license_id);
+        Ok(None)
+    }
+
+    /// Get the name similarity threshold
+    pub fn name_similarity_threshold(&self) -> f64 {
+        self.name_similarity_threshold
+    }
+
+    /// Get the body similarity threshold
+    pub fn body_similarity_threshold(&self) -> f64 {
+        self.body_similarity_threshold
+    }
+
+    /// Update thresholds
+    pub fn set_thresholds(&mut self, name_threshold: f64, body_threshold: f64) {
+        self.name_similarity_threshold = name_threshold;
+        self.body_similarity_threshold = body_threshold;
+    }
+
+    /// Find all licenses above a similarity threshold
+    pub async fn find_all_similar(&self, input: &str, threshold: f64) -> Result<Vec<Score>> {
+        let input_content = Content::from(input);
+        let mut scores = Vec::new();
+
+        for license in self.catalogue.licenses() {
+            // Check ID similarity
+            let id_content = Content::from(license.id.as_str());
+            let id_score = input_content.similarity_score(&id_content);
+            
+            if id_score >= threshold {
+                scores.push(Score::new(license.id.clone(), id_score));
+            }
+
+            // Check name similarity
+            let name_content = Content::from(license.name.as_str());
+            let name_score = input_content.similarity_score(&name_content);
+            
+            if name_score >= threshold {
+                scores.push(Score::new(format!("{} (name)", license.id), name_score));
+            }
+        }
+
+        // Sort by score descending
+        scores.sort_by(|a, b| b.score().partial_cmp(&a.score()).unwrap_or(std::cmp::Ordering::Equal));
+        
+        Ok(scores)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::collections::HashMap;
+
+    fn create_test_catalogue() -> Catalogue {
+        // Create a minimal catalogue for testing
+        Catalogue::default() // This will use the built-in SPDX licenses
+    }
+
+    #[tokio::test]
+    async fn test_guess_creation() {
+        let catalogue = create_test_catalogue();
+        let guess = Guess::new(catalogue);
+        
+        assert_eq!(guess.name_similarity_threshold(), 85.0);
+        assert_eq!(guess.body_similarity_threshold(), 89.0);
+    }
+
+    #[tokio::test]
+    async fn test_guess_with_custom_thresholds() {
+        let catalogue = create_test_catalogue();
+        let guess = Guess::with_thresholds(catalogue, 80.0, 85.0);
+        
+        assert_eq!(guess.name_similarity_threshold(), 80.0);
+        assert_eq!(guess.body_similarity_threshold(), 85.0);
+    }
+
+    #[tokio::test]
+    async fn test_detect_exact_match() {
+        let catalogue = create_test_catalogue();
+        let guess = Guess::new(catalogue);
+        
+        let result = guess.detect_license("MIT".into()).await.unwrap();
+        assert_eq!(result, "MIT");
+    }
+
+    #[tokio::test]
+    async fn test_detect_empty_string() {
+        let catalogue = create_test_catalogue();
+        let guess = Guess::new(catalogue);
+        
+        let result = guess.detect_license("".into()).await.unwrap();
+        assert_eq!(result, "unknown");
+        
+        let result = guess.detect_license("   ".into()).await.unwrap();
+        assert_eq!(result, "unknown");
+    }
+
+    #[tokio::test]
+    async fn test_detect_spdx_expression() {
+        let catalogue = create_test_catalogue();
+        let guess = Guess::new(catalogue);
+        
+        let result = guess.detect_license("MIT AND Apache-2.0".into()).await.unwrap();
+        assert_eq!(result, "MIT AND Apache-2.0");
+        
+        let result = guess.detect_license("(MIT OR Apache-2.0)".into()).await.unwrap();
+        assert_eq!(result, "(MIT OR Apache-2.0)");
+    }
+
+    #[tokio::test]
+    async fn test_detect_from_array() {
+        let catalogue = create_test_catalogue();
+        let guess = Guess::new(catalogue);
+        
+        let licenses = vec!["MIT".to_string(), "Apache-2.0".to_string()];
+        let result = guess.detect_license(licenses.into()).await.unwrap();
+        assert_eq!(result, "MIT"); // Should return first match
+        
+        let empty_array: Vec<String> = vec![];
+        let result = guess.detect_license(empty_array.into()).await.unwrap();
+        assert_eq!(result, "unknown");
+    }
+
+    #[tokio::test]
+    async fn test_detect_from_hash() {
+        let catalogue = create_test_catalogue();
+        let guess = Guess::new(catalogue);
+        
+        let mut metadata = HashMap::new();
+        metadata.insert("license".to_string(), "MIT".to_string());
+        metadata.insert("author".to_string(), "Someone".to_string());
+        
+        let result = guess.detect_license(metadata.into()).await.unwrap();
+        assert_eq!(result, "MIT");
+    }
+
+    #[tokio::test]
+    async fn test_detect_from_hash_no_license() {
+        let catalogue = create_test_catalogue();
+        let guess = Guess::new(catalogue);
+        
+        let mut metadata = HashMap::new();
+        metadata.insert("author".to_string(), "Someone".to_string());
+        metadata.insert("version".to_string(), "1.0.0".to_string());
+        
+        let result = guess.detect_license(metadata.into()).await.unwrap();
+        assert_eq!(result, "unknown");
+    }
+
+    #[tokio::test]
+    async fn test_guess_input_conversions() {
+        // Test string conversions
+        let input1: GuessInput = "MIT".into();
+        let input2: GuessInput = "MIT".to_string().into();
+        
+        match (input1, input2) {
+            (GuessInput::String(s1), GuessInput::String(s2)) => {
+                assert_eq!(s1, "MIT");
+                assert_eq!(s2, "MIT");
+            }
+            _ => panic!("Expected String variants"),
+        }
+        
+        // Test array conversions
+        let input3: GuessInput = vec!["MIT", "Apache-2.0"].into();
+        let input4: GuessInput = vec!["MIT".to_string(), "Apache-2.0".to_string()].into();
+        
+        match (input3, input4) {
+            (GuessInput::Array(a1), GuessInput::Array(a2)) => {
+                assert_eq!(a1, vec!["MIT", "Apache-2.0"]);
+                assert_eq!(a2, vec!["MIT", "Apache-2.0"]);
+            }
+            _ => panic!("Expected Array variants"),
+        }
+    }
+
+    #[tokio::test]
+    async fn test_find_all_similar() {
+        let catalogue = create_test_catalogue();
+        let guess = Guess::new(catalogue);
+        
+        let scores = guess.find_all_similar("MIT License", 50.0).await.unwrap();
+        
+        // Should find some matches with MIT in the name
+        assert!(!scores.is_empty());
+        
+        // Check that scores are sorted in descending order
+        for i in 1..scores.len() {
+            assert!(scores[i-1].score() >= scores[i].score());
+        }
+    }
+
+    #[tokio::test]
+    async fn test_threshold_updates() {
+        let catalogue = create_test_catalogue();
+        let mut guess = Guess::new(catalogue);
+        
+        guess.set_thresholds(75.0, 80.0);
+        assert_eq!(guess.name_similarity_threshold(), 75.0);
+        assert_eq!(guess.body_similarity_threshold(), 80.0);
+    }
+}
\ No newline at end of file
src/core/http.rs
@@ -0,0 +1,253 @@
+use reqwest::{Client, Response, StatusCode};
+use std::collections::HashMap;
+use std::time::Duration;
+use thiserror::Error;
+use tracing::debug;
+use url::Url;
+
+use super::circuit::CircuitBreaker;
+
+#[derive(Error, Debug)]
+pub enum HttpError {
+    #[error("Request failed: {0}")]
+    RequestFailed(#[from] reqwest::Error),
+    #[error("Circuit breaker open for host: {0}")]
+    CircuitBreakerOpen(String),
+    #[error("Too many redirects")]
+    TooManyRedirects,
+    #[error("Invalid URL: {0}")]
+    InvalidUrl(String),
+    #[error("HTTP error {status}: {message}")]
+    HttpStatus { status: StatusCode, message: String },
+    #[error("Network operation disabled in airgap mode")]
+    AirgapMode,
+}
+
+pub type HttpResult<T> = Result<T, HttpError>;
+
+#[derive(Debug, Clone)]
+pub struct HttpClient {
+    client: Client,
+    circuit_breakers: HashMap<String, CircuitBreaker>,
+    max_redirects: usize,
+}
+
+impl HttpClient {
+    pub fn new() -> Self {
+        let client = Client::builder()
+            .timeout(Duration::from_secs(30))
+            .connect_timeout(Duration::from_secs(10))
+            .redirect(reqwest::redirect::Policy::none()) // Handle redirects manually
+            .user_agent("spandx-rs/0.1.0")
+            .build()
+            .expect("Failed to create HTTP client");
+
+        Self {
+            client,
+            circuit_breakers: HashMap::new(),
+            max_redirects: 3,
+        }
+    }
+
+    pub async fn get(&mut self, url: &str) -> HttpResult<Response> {
+        if crate::is_airgap_mode() {
+            return Err(HttpError::AirgapMode);
+        }
+
+        let parsed_url = Url::parse(url)
+            .map_err(|_| HttpError::InvalidUrl(url.to_string()))?;
+
+        let host = parsed_url.host_str()
+            .ok_or_else(|| HttpError::InvalidUrl("No host in URL".to_string()))?
+            .to_string();
+
+        // Check circuit breaker state first
+        let is_open = self.circuit_breakers
+            .get(&host)
+            .map(|cb| cb.is_open())
+            .unwrap_or(false);
+
+        if is_open {
+            return Err(HttpError::CircuitBreakerOpen(host));
+        }
+
+        // Make the request
+        let result = self.make_request(url, 0).await;
+
+        // Update circuit breaker based on result
+        let circuit_breaker = self.circuit_breakers
+            .entry(host)
+            .or_insert_with(CircuitBreaker::new);
+
+        match &result {
+            Ok(_) => circuit_breaker.record_success(),
+            Err(_) => circuit_breaker.record_failure(),
+        }
+
+        result
+    }
+
+    async fn make_request(&self, url: &str, redirect_count: usize) -> HttpResult<Response> {
+        if redirect_count > self.max_redirects {
+            return Err(HttpError::TooManyRedirects);
+        }
+
+        debug!("Making HTTP GET request to: {}", url);
+        
+        let response = self.client
+            .get(url)
+            .send()
+            .await?;
+
+        let status = response.status();
+        
+        if status.is_redirection() {
+            if let Some(location) = response.headers().get("location") {
+                let location_str = location.to_str()
+                    .map_err(|_| HttpError::InvalidUrl("Invalid redirect location".to_string()))?;
+                
+                // Handle relative URLs
+                let redirect_url = if location_str.starts_with("http") {
+                    location_str.to_string()
+                } else {
+                    let base = Url::parse(url)
+                        .map_err(|_| HttpError::InvalidUrl(url.to_string()))?;
+                    base.join(location_str)
+                        .map_err(|_| HttpError::InvalidUrl("Invalid redirect URL".to_string()))?
+                        .to_string()
+                };
+
+                debug!("Following redirect to: {}", redirect_url);
+                return Box::pin(self.make_request(&redirect_url, redirect_count + 1)).await;
+            }
+        }
+
+        if !status.is_success() {
+            let error_text = response.text().await.unwrap_or_default();
+            return Err(HttpError::HttpStatus {
+                status,
+                message: error_text,
+            });
+        }
+
+        Ok(response)
+    }
+
+    pub async fn get_json<T>(&mut self, url: &str) -> HttpResult<T>
+    where
+        T: serde::de::DeserializeOwned,
+    {
+        let response = self.get(url).await?;
+        let json = response.json::<T>().await?;
+        Ok(json)
+    }
+
+    pub async fn get_text(&mut self, url: &str) -> HttpResult<String> {
+        let response = self.get(url).await?;
+        let text = response.text().await?;
+        Ok(text)
+    }
+
+    pub fn reset_circuit_breaker(&mut self, host: &str) {
+        if let Some(cb) = self.circuit_breakers.get_mut(host) {
+            cb.reset();
+        }
+    }
+
+    pub fn get_circuit_breaker_status(&self, host: &str) -> Option<bool> {
+        self.circuit_breakers.get(host).map(|cb| cb.is_open())
+    }
+}
+
+impl Default for HttpClient {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use wiremock::matchers::{method, path};
+    use wiremock::{Mock, MockServer, ResponseTemplate};
+
+    #[tokio::test]
+    async fn test_http_client_get() {
+        let mock_server = MockServer::start().await;
+
+        Mock::given(method("GET"))
+            .and(path("/test"))
+            .respond_with(ResponseTemplate::new(200).set_body_string("Hello, World!"))
+            .mount(&mock_server)
+            .await;
+
+        let mut client = HttpClient::new();
+        let url = format!("{}/test", mock_server.uri());
+        
+        let response = client.get(&url).await.unwrap();
+        assert_eq!(response.status(), StatusCode::OK);
+        
+        let text = response.text().await.unwrap();
+        assert_eq!(text, "Hello, World!");
+    }
+
+    #[tokio::test]
+    async fn test_http_client_json() {
+        let mock_server = MockServer::start().await;
+
+        Mock::given(method("GET"))
+            .and(path("/json"))
+            .respond_with(
+                ResponseTemplate::new(200)
+                    .set_body_json(serde_json::json!({"message": "Hello, JSON!"}))
+            )
+            .mount(&mock_server)
+            .await;
+
+        let mut client = HttpClient::new();
+        let url = format!("{}/json", mock_server.uri());
+        
+        let json: serde_json::Value = client.get_json(&url).await.unwrap();
+        assert_eq!(json["message"], "Hello, JSON!");
+    }
+
+    #[tokio::test]
+    async fn test_http_client_redirect() {
+        let mock_server = MockServer::start().await;
+
+        Mock::given(method("GET"))
+            .and(path("/redirect"))
+            .respond_with(
+                ResponseTemplate::new(302)
+                    .insert_header("location", format!("{}/final", mock_server.uri()).as_str())
+            )
+            .mount(&mock_server)
+            .await;
+
+        Mock::given(method("GET"))
+            .and(path("/final"))
+            .respond_with(ResponseTemplate::new(200).set_body_string("Final destination"))
+            .mount(&mock_server)
+            .await;
+
+        let mut client = HttpClient::new();
+        let url = format!("{}/redirect", mock_server.uri());
+        
+        let response = client.get(&url).await.unwrap();
+        let text = response.text().await.unwrap();
+        assert_eq!(text, "Final destination");
+    }
+
+    #[tokio::test]
+    async fn test_airgap_mode() {
+        crate::set_airgap_mode(true);
+        
+        let mut client = HttpClient::new();
+        let result = client.get("https://example.com").await;
+        
+        assert!(matches!(result, Err(HttpError::AirgapMode)));
+        
+        // Reset for other tests
+        crate::set_airgap_mode(false);
+    }
+}
\ No newline at end of file
src/core/license.rs
@@ -0,0 +1,311 @@
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct License {
+    pub id: String,
+    pub name: String,
+    pub deprecated: bool,
+    pub osi_approved: Option<bool>,
+    pub fsf_libre: Option<bool>,
+    pub reference: String,
+    pub reference_number: Option<u32>,
+    pub details_url: Option<String>,
+    pub see_also: Vec<String>,
+    pub license_text: Option<String>,
+    pub standard_license_header: Option<String>,
+    pub standard_license_template: Option<String>,
+    pub cross_refs: Vec<CrossRef>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct CrossRef {
+    #[serde(rename = "match")]
+    pub match_type: String,
+    pub url: String,
+    pub is_valid: bool,
+    pub is_live: bool,
+    pub timestamp: String,
+    pub is_wayback_link: bool,
+    pub order: u32,
+}
+
+impl License {
+    pub fn new(id: String, name: String) -> Self {
+        Self {
+            id: id.clone(),
+            name,
+            deprecated: false,
+            osi_approved: None,
+            fsf_libre: None,
+            reference: format!("https://spdx.org/licenses/{}.html", id),
+            reference_number: None,
+            details_url: None,
+            see_also: Vec::new(),
+            license_text: None,
+            standard_license_header: None,
+            standard_license_template: None,
+            cross_refs: Vec::new(),
+        }
+    }
+
+    pub fn is_osi_approved(&self) -> bool {
+        self.osi_approved.unwrap_or(false)
+    }
+
+    pub fn is_fsf_libre(&self) -> bool {
+        self.fsf_libre.unwrap_or(false)
+    }
+
+    pub fn is_deprecated(&self) -> bool {
+        self.deprecated
+    }
+
+    pub fn short_identifier(&self) -> &str {
+        &self.id
+    }
+
+    pub fn full_name(&self) -> &str {
+        &self.name
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct LicenseCatalogue {
+    licenses: HashMap<String, License>,
+    version: String,
+    release_date: String,
+}
+
+impl LicenseCatalogue {
+    pub fn new() -> Self {
+        Self {
+            licenses: HashMap::new(),
+            version: String::new(),
+            release_date: String::new(),
+        }
+    }
+
+    pub fn from_json(json_data: &str) -> Result<Self, serde_json::Error> {
+        #[derive(Deserialize)]
+        struct LicenseList {
+            #[serde(rename = "licenseListVersion")]
+            license_list_version: String,
+            #[serde(rename = "releaseDate")]
+            release_date: String,
+            licenses: Vec<License>,
+        }
+
+        let license_list: LicenseList = serde_json::from_str(json_data)?;
+        let mut catalogue = Self::new();
+        catalogue.version = license_list.license_list_version;
+        catalogue.release_date = license_list.release_date;
+
+        for license in license_list.licenses {
+            catalogue.licenses.insert(license.id.clone(), license);
+        }
+
+        Ok(catalogue)
+    }
+
+    pub fn add_license(&mut self, license: License) {
+        self.licenses.insert(license.id.clone(), license);
+    }
+
+    pub fn get_license(&self, id: &str) -> Option<&License> {
+        self.licenses.get(id)
+    }
+
+    pub fn find_by_name(&self, name: &str) -> Option<&License> {
+        self.licenses
+            .values()
+            .find(|license| license.name.eq_ignore_ascii_case(name))
+    }
+
+    pub fn find_similar_by_name(&self, name: &str, threshold: f64) -> Vec<&License> {
+        use crate::core::Content;
+        
+        let mut similar = Vec::new();
+        let input_content = Content::from(name);
+
+        for license in self.licenses.values() {
+            // Check similarity with license ID
+            let id_content = Content::from(license.id.as_str());
+            let id_similarity = input_content.similarity_score(&id_content);
+            
+            // Check similarity with license name  
+            let name_content = Content::from(license.name.as_str());
+            let name_similarity = input_content.similarity_score(&name_content);
+            
+            // Use the higher of the two scores
+            let best_similarity = id_similarity.max(name_similarity);
+            
+            if best_similarity >= threshold {
+                similar.push(license);
+            }
+        }
+
+        // Sort by similarity score (highest first)
+        similar.sort_by(|a, b| {
+            let id_content_a = Content::from(a.id.as_str());
+            let name_content_a = Content::from(a.name.as_str());
+            let score_a = input_content.similarity_score(&id_content_a)
+                .max(input_content.similarity_score(&name_content_a));
+                
+            let id_content_b = Content::from(b.id.as_str());
+            let name_content_b = Content::from(b.name.as_str());
+            let score_b = input_content.similarity_score(&id_content_b)
+                .max(input_content.similarity_score(&name_content_b));
+                
+            score_b.partial_cmp(&score_a).unwrap_or(std::cmp::Ordering::Equal)
+        });
+
+        similar
+    }
+
+    pub fn licenses(&self) -> impl Iterator<Item = &License> {
+        self.licenses.values()
+    }
+
+    pub fn len(&self) -> usize {
+        self.licenses.len()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.licenses.is_empty()
+    }
+
+    pub fn version(&self) -> &str {
+        &self.version
+    }
+
+    pub fn release_date(&self) -> &str {
+        &self.release_date
+    }
+}
+
+impl Default for LicenseCatalogue {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+// Dice coefficient similarity calculation using new Content-based approach
+#[allow(dead_code)]
+fn similarity_score(s1: &str, s2: &str) -> f64 {
+    use crate::core::Content;
+    
+    let content1 = Content::from(s1);
+    let content2 = Content::from(s2);
+    
+    // Convert to 0-1 scale to match old behavior
+    content1.similarity_score(&content2) / 100.0
+}
+
+// Legacy bigram-based similarity (kept for comparison/fallback)
+#[allow(dead_code)]
+fn bigram_similarity_score(s1: &str, s2: &str) -> f64 {
+    if s1 == s2 {
+        return 1.0;
+    }
+    if s1.is_empty() || s2.is_empty() {
+        return 0.0;
+    }
+
+    let bigrams1 = get_bigrams(s1);
+    let bigrams2 = get_bigrams(s2);
+
+    if bigrams1.is_empty() && bigrams2.is_empty() {
+        return 1.0;
+    }
+    if bigrams1.is_empty() || bigrams2.is_empty() {
+        return 0.0;
+    }
+
+    let intersection_size = bigrams1.iter()
+        .filter(|bigram| bigrams2.contains(bigram))
+        .count();
+
+    (2.0 * intersection_size as f64) / (bigrams1.len() + bigrams2.len()) as f64
+}
+
+#[allow(dead_code)]
+fn get_bigrams(s: &str) -> Vec<String> {
+    let chars: Vec<char> = s.chars().collect();
+    if chars.len() < 2 {
+        return vec![s.to_string()];
+    }
+
+    chars.windows(2)
+        .map(|window| window.iter().collect())
+        .collect()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_license_creation() {
+        let license = License::new("MIT".to_string(), "MIT License".to_string());
+        assert_eq!(license.id, "MIT");
+        assert_eq!(license.name, "MIT License");
+        assert!(!license.deprecated);
+    }
+
+    #[test]
+    fn test_license_catalogue() {
+        let mut catalogue = LicenseCatalogue::new();
+        let license = License::new("MIT".to_string(), "MIT License".to_string());
+        
+        catalogue.add_license(license);
+        assert_eq!(catalogue.len(), 1);
+        
+        let retrieved = catalogue.get_license("MIT");
+        assert!(retrieved.is_some());
+        assert_eq!(retrieved.unwrap().name, "MIT License");
+    }
+
+    #[test]
+    fn test_similarity_score() {
+        assert_eq!(similarity_score("hello", "hello"), 1.0);
+        assert_eq!(similarity_score("", ""), 1.0);
+        assert_eq!(similarity_score("hello", ""), 0.0);
+        assert!(similarity_score("hello", "hallo") > 0.0);
+        assert!(similarity_score("hello", "hallo") < 1.0);
+    }
+
+    #[test]
+    fn test_find_similar_licenses() {
+        let mut catalogue = LicenseCatalogue::new();
+        catalogue.add_license(License::new("MIT".to_string(), "MIT License".to_string()));
+        catalogue.add_license(License::new("Apache-2.0".to_string(), "Apache License 2.0".to_string()));
+        
+        let similar = catalogue.find_similar_by_name("MIT", 50.0); // 50% threshold (Content uses 0-100 scale)
+        assert_eq!(similar.len(), 1);
+        assert_eq!(similar[0].id, "MIT");
+    }
+
+    #[test]
+    fn test_content_based_similarity() {
+        let mut catalogue = LicenseCatalogue::new();
+        catalogue.add_license(License::new("MIT".to_string(), "MIT License".to_string()));
+        catalogue.add_license(License::new("Apache-2.0".to_string(), "Apache License 2.0".to_string()));
+        catalogue.add_license(License::new("BSD-3-Clause".to_string(), "BSD 3-Clause License".to_string()));
+        
+        // Test partial name match
+        let similar = catalogue.find_similar_by_name("MIT License", 80.0);
+        assert!(!similar.is_empty());
+        assert_eq!(similar[0].id, "MIT");
+        
+        // Test case insensitive matching
+        let similar = catalogue.find_similar_by_name("mit license", 80.0);
+        assert!(!similar.is_empty());
+        assert_eq!(similar[0].id, "MIT");
+        
+        // Test ID matching
+        let similar = catalogue.find_similar_by_name("mit", 80.0);
+        assert!(!similar.is_empty());
+        assert_eq!(similar[0].id, "MIT");
+    }
+}
\ No newline at end of file
src/core/mod.rs
@@ -0,0 +1,20 @@
+pub mod dependency;
+pub mod license;
+pub mod parser;
+pub mod package_manager;
+pub mod cache;
+pub mod http;
+pub mod git;
+pub mod circuit;
+pub mod content;
+pub mod score;
+pub mod guess;
+pub mod path_traversal;
+
+pub use dependency::*;
+pub use license::*;
+pub use parser::*;
+pub use package_manager::*;
+pub use content::Content;
+pub use score::Score;
+pub use guess::{Guess, GuessInput};
\ No newline at end of file
src/core/package_manager.rs
@@ -0,0 +1,222 @@
+use serde::{Deserialize, Serialize};
+use std::fmt;
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum PackageManager {
+    RubyGems,
+    Npm,
+    Yarn,
+    Python,
+    Pip,
+    Pipenv,
+    Poetry,
+    Maven,
+    Gradle,
+    NuGet,
+    Composer,
+    Go,
+    Cargo,
+    CocoaPods,
+    Carthage,
+    SwiftPM,
+    Conda,
+    Terraform,
+    Docker,
+    Alpine,
+    Debian,
+    Unknown(String),
+}
+
+impl PackageManager {
+    pub fn from_source(source: &str) -> Self {
+        match source.to_lowercase().as_str() {
+            "rubygems" | "ruby" | "gem" => Self::RubyGems,
+            "npm" => Self::Npm,
+            "yarn" => Self::Yarn,
+            "python" | "pypi" => Self::Python,
+            "pip" => Self::Pip,
+            "pipenv" => Self::Pipenv,
+            "poetry" => Self::Poetry,
+            "maven" => Self::Maven,
+            "gradle" => Self::Gradle,
+            "nuget" | ".net" | "dotnet" => Self::NuGet,
+            "composer" | "packagist" | "php" => Self::Composer,
+            "go" | "golang" => Self::Go,
+            "cargo" | "rust" => Self::Cargo,
+            "cocoapods" => Self::CocoaPods,
+            "carthage" => Self::Carthage,
+            "swift" | "spm" => Self::SwiftPM,
+            "conda" => Self::Conda,
+            "terraform" => Self::Terraform,
+            "docker" => Self::Docker,
+            "apk" | "alpine" => Self::Alpine,
+            "dpkg" | "debian" | "apt" => Self::Debian,
+            _ => Self::Unknown(source.to_string()),
+        }
+    }
+
+    pub fn to_source_string(&self) -> &str {
+        match self {
+            Self::RubyGems => "rubygems",
+            Self::Npm => "npm",
+            Self::Yarn => "yarn",
+            Self::Python => "python",
+            Self::Pip => "pip",
+            Self::Pipenv => "pipenv",
+            Self::Poetry => "poetry",
+            Self::Maven => "maven",
+            Self::Gradle => "gradle",
+            Self::NuGet => "nuget",
+            Self::Composer => "composer",
+            Self::Go => "go",
+            Self::Cargo => "cargo",
+            Self::CocoaPods => "cocoapods",
+            Self::Carthage => "carthage",
+            Self::SwiftPM => "swift",
+            Self::Conda => "conda",
+            Self::Terraform => "terraform",
+            Self::Docker => "docker",
+            Self::Alpine => "apk",
+            Self::Debian => "dpkg",
+            Self::Unknown(s) => s,
+        }
+    }
+
+    pub fn display_name(&self) -> &str {
+        match self {
+            Self::RubyGems => "RubyGems",
+            Self::Npm => "NPM",
+            Self::Yarn => "Yarn",
+            Self::Python => "PyPI",
+            Self::Pip => "Pip",
+            Self::Pipenv => "Pipenv",
+            Self::Poetry => "Poetry",
+            Self::Maven => "Maven",
+            Self::Gradle => "Gradle",
+            Self::NuGet => "NuGet",
+            Self::Composer => "Packagist",
+            Self::Go => "Go Modules",
+            Self::Cargo => "Cargo",
+            Self::CocoaPods => "CocoaPods",
+            Self::Carthage => "Carthage",
+            Self::SwiftPM => "Swift Package Manager",
+            Self::Conda => "Conda",
+            Self::Terraform => "Terraform Registry",
+            Self::Docker => "Docker Hub",
+            Self::Alpine => "Alpine Linux",
+            Self::Debian => "Debian",
+            Self::Unknown(s) => s,
+        }
+    }
+
+    pub fn is_javascript(&self) -> bool {
+        matches!(self, Self::Npm | Self::Yarn)
+    }
+
+    pub fn is_python(&self) -> bool {
+        matches!(self, Self::Python | Self::Pip | Self::Pipenv | Self::Poetry | Self::Conda)
+    }
+
+    pub fn is_dotnet(&self) -> bool {
+        matches!(self, Self::NuGet)
+    }
+
+    pub fn is_java(&self) -> bool {
+        matches!(self, Self::Maven | Self::Gradle)
+    }
+
+    pub fn is_ruby(&self) -> bool {
+        matches!(self, Self::RubyGems)
+    }
+
+    pub fn is_php(&self) -> bool {
+        matches!(self, Self::Composer)
+    }
+
+    pub fn is_os_package(&self) -> bool {
+        matches!(self, Self::Alpine | Self::Debian)
+    }
+}
+
+impl fmt::Display for PackageManager {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.display_name())
+    }
+}
+
+impl From<&str> for PackageManager {
+    fn from(source: &str) -> Self {
+        Self::from_source(source)
+    }
+}
+
+impl From<String> for PackageManager {
+    fn from(source: String) -> Self {
+        Self::from_source(&source)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_from_source() {
+        assert_eq!(PackageManager::from_source("rubygems"), PackageManager::RubyGems);
+        assert_eq!(PackageManager::from_source("npm"), PackageManager::Npm);
+        assert_eq!(PackageManager::from_source("yarn"), PackageManager::Yarn);
+        assert_eq!(PackageManager::from_source("python"), PackageManager::Python);
+        assert_eq!(PackageManager::from_source("maven"), PackageManager::Maven);
+        assert_eq!(PackageManager::from_source("nuget"), PackageManager::NuGet);
+        assert_eq!(PackageManager::from_source("composer"), PackageManager::Composer);
+        assert_eq!(PackageManager::from_source("unknown"), PackageManager::Unknown("unknown".to_string()));
+    }
+
+    #[test]
+    fn test_to_source_string() {
+        assert_eq!(PackageManager::RubyGems.to_source_string(), "rubygems");
+        assert_eq!(PackageManager::Npm.to_source_string(), "npm");
+        assert_eq!(PackageManager::Yarn.to_source_string(), "yarn");
+        assert_eq!(PackageManager::Python.to_source_string(), "python");
+    }
+
+    #[test]
+    fn test_display_name() {
+        assert_eq!(PackageManager::RubyGems.display_name(), "RubyGems");
+        assert_eq!(PackageManager::Npm.display_name(), "NPM");
+        assert_eq!(PackageManager::Python.display_name(), "PyPI");
+    }
+
+    #[test]
+    fn test_type_checks() {
+        assert!(PackageManager::Npm.is_javascript());
+        assert!(PackageManager::Yarn.is_javascript());
+        assert!(!PackageManager::RubyGems.is_javascript());
+
+        assert!(PackageManager::Python.is_python());
+        assert!(PackageManager::Pip.is_python());
+        assert!(!PackageManager::Npm.is_python());
+
+        assert!(PackageManager::RubyGems.is_ruby());
+        assert!(!PackageManager::Npm.is_ruby());
+
+        assert!(PackageManager::Alpine.is_os_package());
+        assert!(PackageManager::Debian.is_os_package());
+        assert!(!PackageManager::Npm.is_os_package());
+    }
+
+    #[test]
+    fn test_from_string() {
+        let pm: PackageManager = "rubygems".into();
+        assert_eq!(pm, PackageManager::RubyGems);
+
+        let pm: PackageManager = String::from("npm").into();
+        assert_eq!(pm, PackageManager::Npm);
+    }
+
+    #[test]
+    fn test_display() {
+        assert_eq!(format!("{}", PackageManager::RubyGems), "RubyGems");
+        assert_eq!(format!("{}", PackageManager::Python), "PyPI");
+    }
+}
\ No newline at end of file
src/core/parser.rs
@@ -0,0 +1,191 @@
+use async_trait::async_trait;
+use camino::{Utf8Path, Utf8PathBuf};
+use std::collections::HashSet;
+use thiserror::Error;
+
+use super::DependencyCollection;
+
+#[derive(Error, Debug)]
+pub enum ParserError {
+    #[error("File not found: {0}")]
+    FileNotFound(Utf8PathBuf),
+    #[error("Parse error: {0}")]
+    ParseError(String),
+    #[error("IO error: {0}")]
+    IoError(#[from] std::io::Error),
+    #[error("JSON error: {0}")]
+    JsonError(#[from] serde_json::Error),
+    #[error("YAML error: {0}")]
+    YamlError(#[from] serde_yaml::Error),
+    #[error("XML error: {0}")]
+    XmlError(String),
+    #[error("Unsupported file format: {0}")]
+    UnsupportedFormat(String),
+}
+
+pub type ParserResult<T> = Result<T, ParserError>;
+
+#[async_trait]
+pub trait Parser: Send + Sync {
+    /// Returns true if this parser can handle the given file
+    fn can_parse(&self, path: &Utf8Path) -> bool;
+    
+    /// Parse the file and return dependencies
+    async fn parse(&self, path: &Utf8Path) -> ParserResult<DependencyCollection>;
+    
+    /// Return the name of this parser
+    fn name(&self) -> &'static str;
+    
+    /// Return file patterns this parser supports
+    fn file_patterns(&self) -> Vec<&'static str>;
+}
+
+pub struct ParserRegistry {
+    parsers: Vec<Box<dyn Parser>>,
+}
+
+impl ParserRegistry {
+    pub fn new() -> Self {
+        Self {
+            parsers: Vec::new(),
+        }
+    }
+
+    pub fn register<P: Parser + 'static>(&mut self, parser: P) {
+        self.parsers.push(Box::new(parser));
+    }
+
+    pub fn find_parser(&self, path: &Utf8Path) -> Option<&dyn Parser> {
+        self.parsers
+            .iter()
+            .find(|parser| parser.can_parse(path))
+            .map(|parser| parser.as_ref())
+    }
+
+    pub async fn parse_file(&self, path: &Utf8Path) -> ParserResult<DependencyCollection> {
+        match self.find_parser(path) {
+            Some(parser) => parser.parse(path).await,
+            None => Err(ParserError::UnsupportedFormat(path.to_string())),
+        }
+    }
+
+    pub fn supported_files(&self) -> Vec<&'static str> {
+        let mut patterns = HashSet::new();
+        for parser in &self.parsers {
+            for pattern in parser.file_patterns() {
+                patterns.insert(pattern);
+            }
+        }
+        let mut result: Vec<&'static str> = patterns.into_iter().collect();
+        result.sort();
+        result
+    }
+
+    pub fn parsers(&self) -> &[Box<dyn Parser>] {
+        &self.parsers
+    }
+}
+
+impl Default for ParserRegistry {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+/// Utility functions for parsers
+pub mod utils {
+    use super::*;
+    use std::fs;
+
+    pub async fn read_file_to_string(path: &Utf8Path) -> ParserResult<String> {
+        if !path.exists() {
+            return Err(ParserError::FileNotFound(path.to_path_buf()));
+        }
+        
+        let content = tokio::fs::read_to_string(path).await?;
+        Ok(content)
+    }
+
+    pub fn read_file_to_string_sync(path: &Utf8Path) -> ParserResult<String> {
+        if !path.exists() {
+            return Err(ParserError::FileNotFound(path.to_path_buf()));
+        }
+        
+        let content = fs::read_to_string(path)?;
+        Ok(content)
+    }
+
+    pub fn matches_pattern(filename: &str, pattern: &str) -> bool {
+        match pattern {
+            "*" => true,
+            pattern if pattern.contains('*') => {
+                let regex_pattern = pattern.replace("*", ".*");
+                regex::Regex::new(&regex_pattern)
+                    .map(|re| re.is_match(filename))
+                    .unwrap_or(false)
+            }
+            pattern => filename == pattern,
+        }
+    }
+
+    pub fn extract_filename(path: &Utf8Path) -> Option<&str> {
+        path.file_name()
+    }
+
+    pub fn normalize_version(version: &str) -> String {
+        // Remove common version prefixes
+        let version = version.trim_start_matches("v");
+        let version = version.trim_start_matches("=");
+        let version = version.trim_start_matches("==");
+        let version = version.trim_start_matches("~");
+        let version = version.trim_start_matches("^");
+        let version = version.trim_start_matches(">=");
+        let version = version.trim_start_matches("<=");
+        let version = version.trim_start_matches(">");
+        let version = version.trim_start_matches("<");
+        
+        version.trim().to_string()
+    }
+
+    pub fn sanitize_package_name(name: &str) -> String {
+        name.trim().to_lowercase()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use super::utils::*;
+
+    #[test]
+    fn test_matches_pattern() {
+        assert!(matches_pattern("Gemfile.lock", "Gemfile.lock"));
+        assert!(matches_pattern("package.json", "package.json"));
+        assert!(matches_pattern("yarn.lock", "*.lock"));
+        assert!(matches_pattern("Pipfile.lock", "Pipfile*"));
+        assert!(!matches_pattern("random.txt", "*.lock"));
+    }
+
+    #[test]
+    fn test_normalize_version() {
+        assert_eq!(normalize_version("v1.0.0"), "1.0.0");
+        assert_eq!(normalize_version("==1.0.0"), "1.0.0");
+        assert_eq!(normalize_version("~1.0.0"), "1.0.0");
+        assert_eq!(normalize_version("^1.0.0"), "1.0.0");
+        assert_eq!(normalize_version(">=1.0.0"), "1.0.0");
+        assert_eq!(normalize_version("1.0.0"), "1.0.0");
+    }
+
+    #[test]
+    fn test_sanitize_package_name() {
+        assert_eq!(sanitize_package_name("  Package-Name  "), "package-name");
+        assert_eq!(sanitize_package_name("PACKAGE"), "package");
+    }
+
+    #[tokio::test]
+    async fn test_parser_registry() {
+        let registry = ParserRegistry::new();
+        assert!(registry.parsers().is_empty());
+        assert!(registry.supported_files().is_empty());
+    }
+}
\ No newline at end of file
src/core/path_traversal.rs
@@ -0,0 +1,2 @@
+// Placeholder for path traversal utilities
+// Will be implemented with directory scanning functionality
\ No newline at end of file
src/core/score.rs
@@ -0,0 +1,233 @@
+/// Tracks the best scoring match for license similarity
+#[derive(Debug, Clone, PartialEq)]
+pub struct Score {
+    pub license_id: String,
+    pub score: f64,
+}
+
+impl Score {
+    /// Create a new Score with the given license ID and score
+    pub fn new(license_id: String, score: f64) -> Self {
+        Self { license_id, score }
+    }
+
+    /// Create a Score from string slice
+    pub fn from_str(license_id: &str, score: f64) -> Self {
+        Self::new(license_id.to_string(), score)
+    }
+
+    /// Get the license ID
+    pub fn license_id(&self) -> &str {
+        &self.license_id
+    }
+
+    /// Get the score
+    pub fn score(&self) -> f64 {
+        self.score
+    }
+
+    /// Check if this score is better (higher) than another
+    pub fn is_better_than(&self, other: &Score) -> bool {
+        self.score > other.score
+    }
+
+    /// Check if this score meets or exceeds a threshold
+    pub fn meets_threshold(&self, threshold: f64) -> bool {
+        self.score >= threshold
+    }
+
+    /// Update the score if the new score is better
+    pub fn update_if_better(&mut self, license_id: String, score: f64) -> bool {
+        if score > self.score {
+            self.license_id = license_id;
+            self.score = score;
+            true
+        } else {
+            false
+        }
+    }
+
+    /// Create a zero score (useful for initialization)
+    pub fn zero() -> Self {
+        Self::new("unknown".to_string(), 0.0)
+    }
+
+    /// Check if this is a zero score
+    pub fn is_zero(&self) -> bool {
+        self.score == 0.0
+    }
+
+    /// Check if this score indicates a perfect match
+    pub fn is_perfect(&self) -> bool {
+        (self.score - 100.0).abs() < f64::EPSILON
+    }
+
+    /// Get score as a percentage string
+    pub fn as_percentage(&self) -> String {
+        format!("{:.1}%", self.score)
+    }
+}
+
+impl Default for Score {
+    fn default() -> Self {
+        Self::zero()
+    }
+}
+
+impl std::fmt::Display for Score {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}: {:.1}%", self.license_id, self.score)
+    }
+}
+
+impl PartialOrd for Score {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        self.score.partial_cmp(&other.score)
+    }
+}
+
+impl From<(String, f64)> for Score {
+    fn from((license_id, score): (String, f64)) -> Self {
+        Self::new(license_id, score)
+    }
+}
+
+impl From<(&str, f64)> for Score {
+    fn from((license_id, score): (&str, f64)) -> Self {
+        Self::from_str(license_id, score)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_score_creation() {
+        let score = Score::new("MIT".to_string(), 85.5);
+        assert_eq!(score.license_id(), "MIT");
+        assert_eq!(score.score(), 85.5);
+    }
+
+    #[test]
+    fn test_from_str() {
+        let score = Score::from_str("Apache-2.0", 90.0);
+        assert_eq!(score.license_id(), "Apache-2.0");
+        assert_eq!(score.score(), 90.0);
+    }
+
+    #[test]
+    fn test_is_better_than() {
+        let score1 = Score::new("MIT".to_string(), 85.0);
+        let score2 = Score::new("Apache-2.0".to_string(), 90.0);
+        let score3 = Score::new("BSD".to_string(), 80.0);
+
+        assert!(score2.is_better_than(&score1));
+        assert!(score1.is_better_than(&score3));
+        assert!(!score1.is_better_than(&score2));
+    }
+
+    #[test]
+    fn test_meets_threshold() {
+        let score = Score::new("MIT".to_string(), 85.0);
+        
+        assert!(score.meets_threshold(80.0));
+        assert!(score.meets_threshold(85.0));
+        assert!(!score.meets_threshold(90.0));
+    }
+
+    #[test]
+    fn test_update_if_better() {
+        let mut score = Score::new("MIT".to_string(), 80.0);
+        
+        // Better score should update
+        assert!(score.update_if_better("Apache-2.0".to_string(), 90.0));
+        assert_eq!(score.license_id(), "Apache-2.0");
+        assert_eq!(score.score(), 90.0);
+        
+        // Worse score should not update
+        assert!(!score.update_if_better("BSD".to_string(), 85.0));
+        assert_eq!(score.license_id(), "Apache-2.0");
+        assert_eq!(score.score(), 90.0);
+        
+        // Equal score should not update
+        assert!(!score.update_if_better("GPL".to_string(), 90.0));
+        assert_eq!(score.license_id(), "Apache-2.0");
+        assert_eq!(score.score(), 90.0);
+    }
+
+    #[test]
+    fn test_zero_score() {
+        let score = Score::zero();
+        assert_eq!(score.license_id(), "unknown");
+        assert_eq!(score.score(), 0.0);
+        assert!(score.is_zero());
+        assert!(!score.is_perfect());
+    }
+
+    #[test]
+    fn test_default() {
+        let score = Score::default();
+        assert_eq!(score.license_id(), "unknown");
+        assert_eq!(score.score(), 0.0);
+        assert!(score.is_zero());
+    }
+
+    #[test]
+    fn test_is_perfect() {
+        let perfect_score = Score::new("MIT".to_string(), 100.0);
+        let imperfect_score = Score::new("Apache-2.0".to_string(), 99.9);
+        
+        assert!(perfect_score.is_perfect());
+        assert!(!imperfect_score.is_perfect());
+    }
+
+    #[test]
+    fn test_as_percentage() {
+        let score = Score::new("MIT".to_string(), 85.6789);
+        assert_eq!(score.as_percentage(), "85.7%");
+    }
+
+    #[test]
+    fn test_display() {
+        let score = Score::new("MIT".to_string(), 85.6);
+        assert_eq!(format!("{}", score), "MIT: 85.6%");
+    }
+
+    #[test]
+    fn test_partial_ord() {
+        let score1 = Score::new("MIT".to_string(), 80.0);
+        let score2 = Score::new("Apache-2.0".to_string(), 90.0);
+        let score3 = Score::new("BSD".to_string(), 80.0);
+
+        assert!(score2 > score1);
+        assert!(score1 < score2);
+        assert!(score1 == score3); // Note: PartialEq compares both fields, PartialOrd only score
+    }
+
+    #[test]
+    fn test_from_tuple() {
+        let score1 = Score::from(("MIT".to_string(), 85.0));
+        let score2 = Score::from(("Apache-2.0", 90.0));
+        
+        assert_eq!(score1.license_id(), "MIT");
+        assert_eq!(score1.score(), 85.0);
+        
+        assert_eq!(score2.license_id(), "Apache-2.0");
+        assert_eq!(score2.score(), 90.0);
+    }
+
+    #[test]
+    fn test_edge_cases() {
+        let zero_score = Score::new("Zero".to_string(), 0.0);
+        let negative_score = Score::new("Negative".to_string(), -10.0);
+        let over_hundred = Score::new("Over".to_string(), 150.0);
+        
+        assert!(zero_score.is_zero());
+        assert!(!negative_score.is_zero());
+        assert!(!over_hundred.is_perfect());
+        
+        assert!(over_hundred.is_better_than(&zero_score));
+        assert!(!negative_score.meets_threshold(0.0));
+    }
+}
\ No newline at end of file
src/formatters/csv.rs
@@ -0,0 +1,84 @@
+use async_trait::async_trait;
+use anyhow::Result;
+use csv::WriterBuilder;
+use std::io;
+
+use super::OutputFormatter;
+use crate::core::DependencyCollection;
+
+pub struct CsvFormatter;
+
+impl CsvFormatter {
+    pub fn new() -> Self {
+        Self
+    }
+}
+
+impl Default for CsvFormatter {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[async_trait]
+impl OutputFormatter for CsvFormatter {
+    async fn format(&self, dependencies: &DependencyCollection) -> Result<()> {
+        let mut writer = WriterBuilder::new()
+            .has_headers(true)
+            .from_writer(io::stdout());
+
+        // Write header
+        writer.write_record(&["Name", "Version", "Licenses", "Location"])?;
+
+        // Write dependencies
+        for dep in dependencies.iter() {
+            writer.write_record(&[
+                &dep.name,
+                &dep.version,
+                &dep.license_display(),
+                &dep.location.to_string(),
+            ])?;
+        }
+
+        writer.flush()?;
+        Ok(())
+    }
+
+    fn name(&self) -> &'static str {
+        "csv"
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::core::{Dependency, DependencyCollection};
+
+    #[tokio::test]
+    async fn test_csv_formatter_empty() {
+        let formatter = CsvFormatter::new();
+        let dependencies = DependencyCollection::new();
+        
+        let result = formatter.format(&dependencies).await;
+        assert!(result.is_ok());
+    }
+
+    #[tokio::test]
+    async fn test_csv_formatter_with_dependencies() {
+        let formatter = CsvFormatter::new();
+        let mut dependencies = DependencyCollection::new();
+        
+        let dep = Dependency::new("test".to_string(), "1.0.0".to_string())
+            .with_license("MIT".to_string());
+        dependencies.add(dep);
+        
+        let result = formatter.format(&dependencies).await;
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_formatter_name() {
+        let formatter = CsvFormatter::new();
+        assert_eq!(formatter.name(), "csv");
+    }
+}
\ No newline at end of file
src/formatters/json.rs
@@ -0,0 +1,71 @@
+use async_trait::async_trait;
+use anyhow::Result;
+use serde_json;
+
+use super::OutputFormatter;
+use crate::core::DependencyCollection;
+
+pub struct JsonFormatter;
+
+impl JsonFormatter {
+    pub fn new() -> Self {
+        Self
+    }
+}
+
+impl Default for JsonFormatter {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[async_trait]
+impl OutputFormatter for JsonFormatter {
+    async fn format(&self, dependencies: &DependencyCollection) -> Result<()> {
+        // Output as line-delimited JSON (one JSON object per line)
+        for dep in dependencies.iter() {
+            let json = serde_json::to_string(dep)?;
+            println!("{}", json);
+        }
+
+        Ok(())
+    }
+
+    fn name(&self) -> &'static str {
+        "json"
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::core::{Dependency, DependencyCollection};
+
+    #[tokio::test]
+    async fn test_json_formatter_empty() {
+        let formatter = JsonFormatter::new();
+        let dependencies = DependencyCollection::new();
+        
+        let result = formatter.format(&dependencies).await;
+        assert!(result.is_ok());
+    }
+
+    #[tokio::test]
+    async fn test_json_formatter_with_dependencies() {
+        let formatter = JsonFormatter::new();
+        let mut dependencies = DependencyCollection::new();
+        
+        let dep = Dependency::new("test".to_string(), "1.0.0".to_string())
+            .with_license("MIT".to_string());
+        dependencies.add(dep);
+        
+        let result = formatter.format(&dependencies).await;
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_formatter_name() {
+        let formatter = JsonFormatter::new();
+        assert_eq!(formatter.name(), "json");
+    }
+}
\ No newline at end of file
src/formatters/mod.rs
@@ -0,0 +1,53 @@
+pub mod table;
+pub mod csv;
+pub mod json;
+
+use async_trait::async_trait;
+use std::collections::HashMap;
+use anyhow::Result;
+
+use crate::core::DependencyCollection;
+
+#[async_trait]
+pub trait OutputFormatter: Send + Sync {
+    async fn format(&self, dependencies: &DependencyCollection) -> Result<()>;
+    fn name(&self) -> &'static str;
+}
+
+pub struct FormatterRegistry {
+    formatters: HashMap<String, Box<dyn OutputFormatter>>,
+}
+
+impl FormatterRegistry {
+    pub fn new() -> Self {
+        Self {
+            formatters: HashMap::new(),
+        }
+    }
+
+    pub fn register<F: OutputFormatter + 'static>(&mut self, formatter: F) {
+        self.formatters.insert(formatter.name().to_string(), Box::new(formatter));
+    }
+
+    pub fn register_all(&mut self) {
+        self.register(table::TableFormatter::new());
+        self.register(csv::CsvFormatter::new());
+        self.register(json::JsonFormatter::new());
+    }
+
+    pub fn get_formatter(&self, name: &str) -> Option<&dyn OutputFormatter> {
+        self.formatters.get(name).map(|f| f.as_ref())
+    }
+
+    pub fn supported_formats(&self) -> Vec<&str> {
+        self.formatters.keys().map(|s| s.as_str()).collect()
+    }
+}
+
+impl Default for FormatterRegistry {
+    fn default() -> Self {
+        let mut registry = Self::new();
+        registry.register_all();
+        registry
+    }
+}
\ No newline at end of file
src/formatters/table.rs
@@ -0,0 +1,95 @@
+use async_trait::async_trait;
+use anyhow::Result;
+use tabled::{Table, Tabled};
+
+use super::OutputFormatter;
+use crate::core::DependencyCollection;
+
+pub struct TableFormatter;
+
+impl TableFormatter {
+    pub fn new() -> Self {
+        Self
+    }
+}
+
+impl Default for TableFormatter {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[async_trait]
+impl OutputFormatter for TableFormatter {
+    async fn format(&self, dependencies: &DependencyCollection) -> Result<()> {
+        if dependencies.is_empty() {
+            println!("No dependencies found");
+            return Ok(());
+        }
+
+        let rows: Vec<DependencyRow> = dependencies
+            .iter()
+            .map(|dep| DependencyRow {
+                name: dep.name.clone(),
+                version: dep.version.clone(),
+                licenses: dep.license_display(),
+                location: dep.location.to_string(),
+            })
+            .collect();
+
+        let table = Table::new(rows);
+        println!("{}", table);
+
+        Ok(())
+    }
+
+    fn name(&self) -> &'static str {
+        "table"
+    }
+}
+
+#[derive(Tabled)]
+struct DependencyRow {
+    #[tabled(rename = "Name")]
+    name: String,
+    #[tabled(rename = "Version")]
+    version: String,
+    #[tabled(rename = "Licenses")]
+    licenses: String,
+    #[tabled(rename = "Location")]
+    location: String,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::core::{Dependency, DependencyCollection};
+
+    #[tokio::test]
+    async fn test_table_formatter_empty() {
+        let formatter = TableFormatter::new();
+        let dependencies = DependencyCollection::new();
+        
+        let result = formatter.format(&dependencies).await;
+        assert!(result.is_ok());
+    }
+
+    #[tokio::test]
+    async fn test_table_formatter_with_dependencies() {
+        let formatter = TableFormatter::new();
+        let mut dependencies = DependencyCollection::new();
+        
+        let dep = Dependency::new("test".to_string(), "1.0.0".to_string())
+            .with_license("MIT".to_string());
+        dependencies.add(dep);
+        
+        let result = formatter.format(&dependencies).await;
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_formatter_name() {
+        let formatter = TableFormatter::new();
+        assert_eq!(formatter.name(), "table");
+    }
+}
\ No newline at end of file
src/gateway/registries/mod.rs
@@ -0,0 +1,7 @@
+pub mod rubygems;
+pub mod npm;
+pub mod pypi;
+
+pub use rubygems::RubyGemsGateway;
+pub use npm::NpmGateway;
+pub use pypi::PypiGateway;
\ No newline at end of file
src/gateway/registries/npm.rs
@@ -0,0 +1,394 @@
+use crate::core::{Dependency, PackageManager};
+use crate::gateway::traits::{Gateway, GatewayError, GatewayResult, PackageMetadata, RegistryInfo};
+use crate::gateway::HttpClient;
+use anyhow::Result;
+use async_trait::async_trait;
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+use tracing::{debug, warn};
+
+/// Gateway for fetching package information from NPM registry
+#[derive(Debug)]
+pub struct NpmGateway {
+    http_client: Arc<HttpClient>,
+    base_url: String,
+}
+
+impl NpmGateway {
+    pub fn new(http_client: Arc<HttpClient>) -> Self {
+        Self {
+            http_client,
+            base_url: "https://registry.npmjs.org".to_string(),
+        }
+    }
+
+    pub fn with_custom_registry(http_client: Arc<HttpClient>, base_url: String) -> Self {
+        Self {
+            http_client,
+            base_url,
+        }
+    }
+
+    async fn get_package_info(&self, name: &str, version: &str) -> GatewayResult<NpmPackageResponse> {
+        let encoded_name = urlencoding::encode(name);
+        let url = format!("{}/{}/{}", self.base_url, encoded_name, version);
+        
+        debug!("Fetching NPM package info from: {}", url);
+        
+        match self.http_client.get_json::<NpmPackageResponse>(&url).await {
+            Ok(response) => Ok(response),
+            Err(e) => {
+                warn!("Failed to fetch NPM info for {}@{}: {}", name, version, e);
+                Err(GatewayError::PackageNotFound {
+                    name: name.to_string(),
+                    version: version.to_string(),
+                })
+            }
+        }
+    }
+
+    #[allow(dead_code)]
+    async fn get_package_latest(&self, name: &str) -> GatewayResult<NpmRegistryResponse> {
+        let encoded_name = urlencoding::encode(name);
+        let url = format!("{}/{}", self.base_url, encoded_name);
+        
+        debug!("Fetching NPM package registry info from: {}", url);
+        
+        match self.http_client.get_json::<NpmRegistryResponse>(&url).await {
+            Ok(response) => Ok(response),
+            Err(e) => {
+                warn!("Failed to fetch NPM registry info for {}: {}", name, e);
+                Err(GatewayError::PackageNotFound {
+                    name: name.to_string(),
+                    version: "latest".to_string(),
+                })
+            }
+        }
+    }
+
+    fn extract_licenses(&self, package_info: &NpmPackageResponse) -> Vec<String> {
+        let mut licenses = Vec::new();
+        
+        // Handle different license field formats
+        match &package_info.license {
+            Some(serde_json::Value::String(license)) => {
+                if !license.trim().is_empty() {
+                    licenses.push(license.clone());
+                }
+            }
+            Some(serde_json::Value::Object(license_obj)) => {
+                if let Some(serde_json::Value::String(license_type)) = license_obj.get("type") {
+                    if !license_type.trim().is_empty() {
+                        licenses.push(license_type.clone());
+                    }
+                }
+            }
+            Some(serde_json::Value::Array(license_array)) => {
+                for license_item in license_array {
+                    match license_item {
+                        serde_json::Value::String(license) => {
+                            if !license.trim().is_empty() {
+                                licenses.push(license.clone());
+                            }
+                        }
+                        serde_json::Value::Object(license_obj) => {
+                            if let Some(serde_json::Value::String(license_type)) = license_obj.get("type") {
+                                if !license_type.trim().is_empty() {
+                                    licenses.push(license_type.clone());
+                                }
+                            }
+                        }
+                        _ => {}
+                    }
+                }
+            }
+            _ => {}
+        }
+        
+        // Also check licenses field (plural)
+        if let Some(package_licenses) = &package_info.licenses {
+            for license_item in package_licenses {
+                match license_item {
+                    serde_json::Value::String(license) => {
+                        if !license.trim().is_empty() && !licenses.contains(license) {
+                            licenses.push(license.clone());
+                        }
+                    }
+                    serde_json::Value::Object(license_obj) => {
+                        if let Some(serde_json::Value::String(license_type)) = license_obj.get("type") {
+                            if !license_type.trim().is_empty() && !licenses.contains(license_type) {
+                                licenses.push(license_type.clone());
+                            }
+                        }
+                    }
+                    _ => {}
+                }
+            }
+        }
+        
+        // Remove duplicates and sort
+        licenses.sort();
+        licenses.dedup();
+        
+        debug!("Extracted licenses for {}: {:?}", package_info.name, licenses);
+        licenses
+    }
+}
+
+#[async_trait]
+impl Gateway for NpmGateway {
+    fn matches(&self, dependency: &Dependency) -> bool {
+        if let Some(source) = &dependency.source {
+            let pm = PackageManager::from_source(source);
+            pm.is_javascript()
+        } else {
+            false
+        }
+    }
+
+    async fn licenses_for(&self, dependency: &Dependency) -> Result<Vec<String>> {
+        let package_info = self.get_package_info(&dependency.name, &dependency.version).await?;
+        Ok(self.extract_licenses(&package_info))
+    }
+
+    fn name(&self) -> &'static str {
+        "NPM"
+    }
+
+    fn base_url(&self) -> &str {
+        &self.base_url
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct NpmPackageResponse {
+    name: String,
+    version: String,
+    description: Option<String>,
+    license: Option<serde_json::Value>,
+    licenses: Option<Vec<serde_json::Value>>,
+    homepage: Option<String>,
+    repository: Option<serde_json::Value>,
+    author: Option<serde_json::Value>,
+    contributors: Option<Vec<serde_json::Value>>,
+    dependencies: Option<std::collections::HashMap<String, String>>,
+    #[serde(rename = "devDependencies")]
+    dev_dependencies: Option<std::collections::HashMap<String, String>>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct NpmRegistryResponse {
+    name: String,
+    description: Option<String>,
+    #[serde(rename = "dist-tags")]
+    dist_tags: Option<std::collections::HashMap<String, String>>,
+    versions: std::collections::HashMap<String, NpmPackageResponse>,
+    license: Option<serde_json::Value>,
+    homepage: Option<String>,
+    repository: Option<serde_json::Value>,
+}
+
+impl From<NpmPackageResponse> for PackageMetadata {
+    fn from(response: NpmPackageResponse) -> Self {
+        let registry = RegistryInfo::new(
+            "NPM".to_string(),
+            "https://registry.npmjs.org".to_string(),
+            "npm".to_string(),
+        );
+
+        let repository = match response.repository {
+            Some(serde_json::Value::String(repo)) => Some(repo),
+            Some(serde_json::Value::Object(repo_obj)) => {
+                repo_obj.get("url").and_then(|v| v.as_str()).map(|s| s.to_string())
+            }
+            _ => None,
+        };
+
+        let mut authors = Vec::new();
+        if let Some(author) = response.author {
+            match author {
+                serde_json::Value::String(author_name) => authors.push(author_name),
+                serde_json::Value::Object(author_obj) => {
+                    if let Some(serde_json::Value::String(name)) = author_obj.get("name") {
+                        authors.push(name.clone());
+                    }
+                }
+                _ => {}
+            }
+        }
+
+        if let Some(contributors) = response.contributors {
+            for contributor in contributors {
+                match contributor {
+                    serde_json::Value::String(contributor_name) => {
+                        if !authors.contains(&contributor_name) {
+                            authors.push(contributor_name);
+                        }
+                    }
+                    serde_json::Value::Object(contributor_obj) => {
+                        if let Some(serde_json::Value::String(name)) = contributor_obj.get("name") {
+                            if !authors.contains(name) {
+                                authors.push(name.clone());
+                            }
+                        }
+                    }
+                    _ => {}
+                }
+            }
+        }
+
+        PackageMetadata::new(response.name, response.version, registry)
+            .with_description(response.description)
+            .with_homepage(response.homepage)
+            .with_repository(repository)
+            .with_authors(authors)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use wiremock::{Mock, MockServer, ResponseTemplate};
+    use wiremock::matchers::{method, path};
+
+    #[tokio::test]
+    async fn test_npm_gateway_matches() {
+        let http_client = Arc::new(HttpClient::new());
+        let gateway = NpmGateway::new(http_client);
+
+        let npm_dep = Dependency::new("lodash".to_string(), "4.17.21".to_string())
+            .with_source("npm".to_string());
+
+        let yarn_dep = Dependency::new("react".to_string(), "18.0.0".to_string())
+            .with_source("yarn".to_string());
+
+        let ruby_dep = Dependency::new("rails".to_string(), "7.0.0".to_string())
+            .with_source("rubygems".to_string());
+
+        assert!(gateway.matches(&npm_dep));
+        assert!(gateway.matches(&yarn_dep));
+        assert!(!gateway.matches(&ruby_dep));
+    }
+
+    #[tokio::test]
+    async fn test_get_package_info() {
+        let mock_server = MockServer::start().await;
+        let http_client = Arc::new(HttpClient::new());
+        let gateway = NpmGateway::with_custom_registry(
+            http_client,
+            mock_server.uri(),
+        );
+
+        let response_body = serde_json::json!({
+            "name": "lodash",
+            "version": "4.17.21",
+            "description": "Lodash modular utilities.",
+            "license": "MIT",
+            "homepage": "https://lodash.com/",
+            "repository": {
+                "type": "git",
+                "url": "git+https://github.com/lodash/lodash.git"
+            }
+        });
+
+        Mock::given(method("GET"))
+            .and(path("/lodash/4.17.21"))
+            .respond_with(ResponseTemplate::new(200).set_body_json(&response_body))
+            .mount(&mock_server)
+            .await;
+
+        let dependency = Dependency::new("lodash".to_string(), "4.17.21".to_string())
+            .with_source("npm".to_string());
+
+        let licenses = gateway.licenses_for(&dependency).await.unwrap();
+        assert_eq!(licenses, vec!["MIT"]);
+    }
+
+    #[tokio::test]
+    async fn test_extract_licenses_different_formats() {
+        let http_client = Arc::new(HttpClient::new());
+        let gateway = NpmGateway::new(http_client);
+
+        // Test string license
+        let response1 = NpmPackageResponse {
+            name: "test-pkg1".to_string(),
+            version: "1.0.0".to_string(),
+            description: None,
+            license: Some(serde_json::Value::String("MIT".to_string())),
+            licenses: None,
+            homepage: None,
+            repository: None,
+            author: None,
+            contributors: None,
+            dependencies: None,
+            dev_dependencies: None,
+        };
+        let licenses1 = gateway.extract_licenses(&response1);
+        assert_eq!(licenses1, vec!["MIT"]);
+
+        // Test object license
+        let response2 = NpmPackageResponse {
+            name: "test-pkg2".to_string(),
+            version: "1.0.0".to_string(),
+            description: None,
+            license: Some(serde_json::json!({"type": "Apache-2.0"})),
+            licenses: None,
+            homepage: None,
+            repository: None,
+            author: None,
+            contributors: None,
+            dependencies: None,
+            dev_dependencies: None,
+        };
+        let licenses2 = gateway.extract_licenses(&response2);
+        assert_eq!(licenses2, vec!["Apache-2.0"]);
+
+        // Test array of licenses
+        let response3 = NpmPackageResponse {
+            name: "test-pkg3".to_string(),
+            version: "1.0.0".to_string(),
+            description: None,
+            license: None,
+            licenses: Some(vec![
+                serde_json::Value::String("MIT".to_string()),
+                serde_json::json!({"type": "BSD-3-Clause"}),
+            ]),
+            homepage: None,
+            repository: None,
+            author: None,
+            contributors: None,
+            dependencies: None,
+            dev_dependencies: None,
+        };
+        let licenses3 = gateway.extract_licenses(&response3);
+        assert_eq!(licenses3, vec!["BSD-3-Clause", "MIT"]);
+    }
+
+    #[tokio::test]
+    async fn test_scoped_package_url_encoding() {
+        let mock_server = MockServer::start().await;
+        let http_client = Arc::new(HttpClient::new());
+        let gateway = NpmGateway::with_custom_registry(
+            http_client,
+            mock_server.uri(),
+        );
+
+        let response_body = serde_json::json!({
+            "name": "@types/node",
+            "version": "18.0.0",
+            "license": "MIT"
+        });
+
+        Mock::given(method("GET"))
+            .and(path("/@types%2Fnode/18.0.0"))
+            .respond_with(ResponseTemplate::new(200).set_body_json(&response_body))
+            .mount(&mock_server)
+            .await;
+
+        let dependency = Dependency::new("@types/node".to_string(), "18.0.0".to_string())
+            .with_source("npm".to_string());
+
+        let licenses = gateway.licenses_for(&dependency).await.unwrap();
+        assert_eq!(licenses, vec!["MIT"]);
+    }
+}
\ No newline at end of file
src/gateway/registries/pypi.rs
@@ -0,0 +1,350 @@
+use crate::core::{Dependency, PackageManager};
+use crate::gateway::traits::{Gateway, GatewayError, GatewayResult, PackageMetadata, RegistryInfo};
+use crate::gateway::HttpClient;
+use anyhow::Result;
+use async_trait::async_trait;
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+use tracing::{debug, warn};
+
+/// Gateway for fetching package information from PyPI
+#[derive(Debug)]
+pub struct PypiGateway {
+    http_client: Arc<HttpClient>,
+    base_url: String,
+}
+
+impl PypiGateway {
+    pub fn new(http_client: Arc<HttpClient>) -> Self {
+        Self {
+            http_client,
+            base_url: "https://pypi.org".to_string(),
+        }
+    }
+
+    pub fn with_custom_index(http_client: Arc<HttpClient>, base_url: String) -> Self {
+        Self {
+            http_client,
+            base_url,
+        }
+    }
+
+    async fn get_package_info(&self, name: &str, version: &str) -> GatewayResult<PypiResponse> {
+        let url = format!("{}/pypi/{}/{}/json", self.base_url, name, version);
+        
+        debug!("Fetching PyPI package info from: {}", url);
+        
+        match self.http_client.get_json::<PypiResponse>(&url).await {
+            Ok(response) => Ok(response),
+            Err(e) => {
+                warn!("Failed to fetch PyPI info for {}@{}: {}", name, version, e);
+                Err(GatewayError::PackageNotFound {
+                    name: name.to_string(),
+                    version: version.to_string(),
+                })
+            }
+        }
+    }
+
+    fn extract_licenses(&self, package_info: &PypiInfo) -> Vec<String> {
+        let mut licenses = Vec::new();
+        
+        // Extract from license field
+        if let Some(license) = &package_info.license {
+            if !license.trim().is_empty() && license != "UNKNOWN" {
+                licenses.push(license.clone());
+            }
+        }
+        
+        // Extract from classifiers
+        if let Some(classifiers) = &package_info.classifiers {
+            for classifier in classifiers {
+                if classifier.starts_with("License ::") {
+                    // Extract license name from classifier
+                    // e.g., "License :: OSI Approved :: MIT License" -> "MIT"
+                    if let Some(license_part) = classifier.split(" :: ").last() {
+                        let license_name = license_part
+                            .replace(" License", "")
+                            .replace("GNU ", "")
+                            .replace("Library or ", "")
+                            .trim()
+                            .to_string();
+                        
+                        if !license_name.is_empty() 
+                            && license_name != "OSI Approved"
+                            && !licenses.contains(&license_name) {
+                            licenses.push(license_name);
+                        }
+                    }
+                }
+            }
+        }
+        
+        // Remove duplicates and sort
+        licenses.sort();
+        licenses.dedup();
+        
+        debug!("Extracted licenses for {}: {:?}", package_info.name, licenses);
+        licenses
+    }
+}
+
+#[async_trait]
+impl Gateway for PypiGateway {
+    fn matches(&self, dependency: &Dependency) -> bool {
+        if let Some(source) = &dependency.source {
+            let pm = PackageManager::from_source(source);
+            pm.is_python()
+        } else {
+            false
+        }
+    }
+
+    async fn licenses_for(&self, dependency: &Dependency) -> Result<Vec<String>> {
+        let response = self.get_package_info(&dependency.name, &dependency.version).await?;
+        Ok(self.extract_licenses(&response.info))
+    }
+
+    fn name(&self) -> &'static str {
+        "PyPI"
+    }
+
+    fn base_url(&self) -> &str {
+        &self.base_url
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct PypiResponse {
+    info: PypiInfo,
+    urls: Vec<PypiUrl>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct PypiInfo {
+    name: String,
+    version: String,
+    summary: Option<String>,
+    description: Option<String>,
+    license: Option<String>,
+    home_page: Option<String>,
+    project_url: Option<String>,
+    project_urls: Option<std::collections::HashMap<String, String>>,
+    author: Option<String>,
+    author_email: Option<String>,
+    maintainer: Option<String>,
+    maintainer_email: Option<String>,
+    classifiers: Option<Vec<String>>,
+    keywords: Option<String>,
+    requires_dist: Option<Vec<String>>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct PypiUrl {
+    filename: String,
+    url: String,
+    #[serde(rename = "packagetype")]
+    package_type: String,
+}
+
+impl From<PypiResponse> for PackageMetadata {
+    fn from(response: PypiResponse) -> Self {
+        let registry = RegistryInfo::new(
+            "PyPI".to_string(),
+            "https://pypi.org".to_string(),
+            "python".to_string(),
+        );
+
+        let mut authors = Vec::new();
+        if let Some(author) = response.info.author {
+            if !author.trim().is_empty() {
+                authors.push(author);
+            }
+        }
+        if let Some(maintainer) = response.info.maintainer {
+            if !maintainer.trim().is_empty() && !authors.contains(&maintainer) {
+                authors.push(maintainer);
+            }
+        }
+
+        // Extract repository URL from project_urls
+        let repository = response.info.project_urls
+            .as_ref()
+            .and_then(|urls| {
+                urls.get("Source")
+                    .or_else(|| urls.get("Repository"))
+                    .or_else(|| urls.get("Homepage"))
+                    .cloned()
+            })
+            .or_else(|| response.info.home_page.clone());
+
+        let dependencies = response.info.requires_dist.unwrap_or_default();
+
+        PackageMetadata::new(response.info.name, response.info.version, registry)
+            .with_description(response.info.summary)
+            .with_homepage(response.info.home_page)
+            .with_repository(repository)
+            .with_authors(authors)
+            .with_dependencies(dependencies)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use wiremock::{Mock, MockServer, ResponseTemplate};
+    use wiremock::matchers::{method, path};
+
+    #[tokio::test]
+    async fn test_pypi_gateway_matches() {
+        let http_client = Arc::new(HttpClient::new());
+        let gateway = PypiGateway::new(http_client);
+
+        let python_dep = Dependency::new("requests".to_string(), "2.28.0".to_string())
+            .with_source("python".to_string());
+
+        let npm_dep = Dependency::new("lodash".to_string(), "4.17.21".to_string())
+            .with_source("npm".to_string());
+
+        assert!(gateway.matches(&python_dep));
+        assert!(!gateway.matches(&npm_dep));
+    }
+
+    #[tokio::test]
+    async fn test_get_package_info() {
+        let mock_server = MockServer::start().await;
+        let http_client = Arc::new(HttpClient::new());
+        let gateway = PypiGateway::with_custom_index(
+            http_client,
+            mock_server.uri(),
+        );
+
+        let response_body = serde_json::json!({
+            "info": {
+                "name": "requests",
+                "version": "2.28.0",
+                "summary": "Python HTTP for Humans.",
+                "license": "Apache 2.0",
+                "home_page": "https://requests.readthedocs.io",
+                "author": "Kenneth Reitz",
+                "classifiers": [
+                    "License :: OSI Approved :: Apache Software License"
+                ]
+            },
+            "urls": []
+        });
+
+        Mock::given(method("GET"))
+            .and(path("/pypi/requests/2.28.0/json"))
+            .respond_with(ResponseTemplate::new(200).set_body_json(&response_body))
+            .mount(&mock_server)
+            .await;
+
+        let dependency = Dependency::new("requests".to_string(), "2.28.0".to_string())
+            .with_source("python".to_string());
+
+        let licenses = gateway.licenses_for(&dependency).await.unwrap();
+        assert!(licenses.contains(&"Apache 2.0".to_string()) || licenses.contains(&"Apache Software".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_extract_licenses_from_classifiers() {
+        let http_client = Arc::new(HttpClient::new());
+        let gateway = PypiGateway::new(http_client);
+
+        let package_info = PypiInfo {
+            name: "test-package".to_string(),
+            version: "1.0.0".to_string(),
+            summary: None,
+            description: None,
+            license: None,
+            home_page: None,
+            project_url: None,
+            project_urls: None,
+            author: None,
+            author_email: None,
+            maintainer: None,
+            maintainer_email: None,
+            classifiers: Some(vec![
+                "Development Status :: 5 - Production/Stable".to_string(),
+                "License :: OSI Approved :: MIT License".to_string(),
+                "Programming Language :: Python :: 3".to_string(),
+            ]),
+            keywords: None,
+            requires_dist: None,
+        };
+
+        let licenses = gateway.extract_licenses(&package_info);
+        assert_eq!(licenses, vec!["MIT"]);
+    }
+
+    #[tokio::test]
+    async fn test_extract_licenses_multiple_sources() {
+        let http_client = Arc::new(HttpClient::new());
+        let gateway = PypiGateway::new(http_client);
+
+        let package_info = PypiInfo {
+            name: "test-package".to_string(),
+            version: "1.0.0".to_string(),
+            summary: None,
+            description: None,
+            license: Some("BSD".to_string()),
+            home_page: None,
+            project_url: None,
+            project_urls: None,
+            author: None,
+            author_email: None,
+            maintainer: None,
+            maintainer_email: None,
+            classifiers: Some(vec![
+                "License :: OSI Approved :: MIT License".to_string(),
+                "License :: OSI Approved :: Apache Software License".to_string(),
+            ]),
+            keywords: None,
+            requires_dist: None,
+        };
+
+        let licenses = gateway.extract_licenses(&package_info);
+        // Should include all unique licenses, sorted
+        assert!(licenses.contains(&"BSD".to_string()));
+        assert!(licenses.contains(&"MIT".to_string()));
+        assert!(licenses.contains(&"Apache Software".to_string()));
+    }
+
+    #[test]
+    fn test_package_metadata_conversion() {
+        let response = PypiResponse {
+            info: PypiInfo {
+                name: "requests".to_string(),
+                version: "2.28.0".to_string(),
+                summary: Some("Python HTTP for Humans.".to_string()),
+                description: None,
+                license: Some("Apache 2.0".to_string()),
+                home_page: Some("https://requests.readthedocs.io".to_string()),
+                project_url: None,
+                project_urls: Some([
+                    ("Source".to_string(), "https://github.com/psf/requests".to_string()),
+                ].into_iter().collect()),
+                author: Some("Kenneth Reitz".to_string()),
+                author_email: None,
+                maintainer: None,
+                maintainer_email: None,
+                classifiers: None,
+                keywords: None,
+                requires_dist: Some(vec!["urllib3>=1.21.1".to_string()]),
+            },
+            urls: vec![],
+        };
+
+        let metadata: PackageMetadata = response.into();
+
+        assert_eq!(metadata.name, "requests");
+        assert_eq!(metadata.version, "2.28.0");
+        assert_eq!(metadata.description, Some("Python HTTP for Humans.".to_string()));
+        assert_eq!(metadata.homepage, Some("https://requests.readthedocs.io".to_string()));
+        assert_eq!(metadata.repository, Some("https://github.com/psf/requests".to_string()));
+        assert_eq!(metadata.authors, vec!["Kenneth Reitz"]);
+        assert_eq!(metadata.dependencies, vec!["urllib3>=1.21.1"]);
+        assert_eq!(metadata.registry.name, "PyPI");
+    }
+}
\ No newline at end of file
src/gateway/registries/rubygems.rs
@@ -0,0 +1,326 @@
+use crate::core::{Dependency, PackageManager};
+use crate::gateway::traits::{Gateway, GatewayError, GatewayResult, PackageMetadata, RegistryInfo};
+use crate::gateway::HttpClient;
+use anyhow::Result;
+use async_trait::async_trait;
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+use tracing::{debug, warn};
+
+/// Gateway for fetching package information from RubyGems.org
+#[derive(Debug)]
+pub struct RubyGemsGateway {
+    http_client: Arc<HttpClient>,
+    base_url: String,
+    api_base_url: String,
+}
+
+impl RubyGemsGateway {
+    pub fn new(http_client: Arc<HttpClient>) -> Self {
+        Self {
+            http_client,
+            base_url: "https://rubygems.org".to_string(),
+            api_base_url: "https://rubygems.org/api/v2".to_string(),
+        }
+    }
+
+    pub fn with_custom_url(http_client: Arc<HttpClient>, base_url: String) -> Self {
+        let api_base_url = format!("{}/api/v2", base_url);
+        Self {
+            http_client,
+            base_url,
+            api_base_url,
+        }
+    }
+
+    async fn get_gem_info(&self, name: &str, version: &str) -> GatewayResult<RubyGemsResponse> {
+        let url = format!("{}/rubygems/{}/versions/{}.json", self.api_base_url, name, version);
+        
+        debug!("Fetching RubyGems info from: {}", url);
+        
+        match self.http_client.get_json::<RubyGemsResponse>(&url).await {
+            Ok(response) => Ok(response),
+            Err(e) => {
+                warn!("Failed to fetch RubyGems info for {}@{}: {}", name, version, e);
+                Err(GatewayError::PackageNotFound {
+                    name: name.to_string(),
+                    version: version.to_string(),
+                })
+            }
+        }
+    }
+
+    fn extract_licenses(&self, gem_info: &RubyGemsResponse) -> Vec<String> {
+        let mut licenses = Vec::new();
+        
+        // Extract from licenses array
+        if let Some(gem_licenses) = &gem_info.licenses {
+            for license in gem_licenses {
+                if !license.trim().is_empty() {
+                    licenses.push(license.clone());
+                }
+            }
+        }
+        
+        // Extract from license field (singular)
+        if let Some(license) = &gem_info.license {
+            if !license.trim().is_empty() && !licenses.contains(license) {
+                licenses.push(license.clone());
+            }
+        }
+        
+        // Remove duplicates and clean up
+        licenses.sort();
+        licenses.dedup();
+        
+        debug!("Extracted licenses for {}: {:?}", gem_info.name, licenses);
+        licenses
+    }
+
+    pub async fn get_all_gems(&self) -> GatewayResult<Vec<String>> {
+        let url = "https://index.rubygems.org/versions";
+        
+        debug!("Fetching all gems from: {}", url);
+        
+        match self.http_client.get_text(url).await {
+            Ok(content) => {
+                let gems: Vec<String> = content
+                    .lines()
+                    .filter_map(|line| {
+                        let parts: Vec<&str> = line.split(' ').collect();
+                        if parts.len() >= 2 {
+                            Some(parts[0].to_string())
+                        } else {
+                            None
+                        }
+                    })
+                    .collect();
+                
+                debug!("Found {} gems in index", gems.len());
+                Ok(gems)
+            }
+            Err(e) => {
+                warn!("Failed to fetch gems index: {}", e);
+                Err(GatewayError::Registry {
+                    message: format!("Failed to fetch gems index: {}", e),
+                })
+            }
+        }
+    }
+}
+
+#[async_trait]
+impl Gateway for RubyGemsGateway {
+    fn matches(&self, dependency: &Dependency) -> bool {
+        if let Some(source) = &dependency.source {
+            let pm = PackageManager::from_source(source);
+            pm.is_ruby()
+        } else {
+            false
+        }
+    }
+
+    async fn licenses_for(&self, dependency: &Dependency) -> Result<Vec<String>> {
+        let gem_info = self.get_gem_info(&dependency.name, &dependency.version).await?;
+        Ok(self.extract_licenses(&gem_info))
+    }
+
+    fn name(&self) -> &'static str {
+        "RubyGems"
+    }
+
+    fn base_url(&self) -> &str {
+        &self.base_url
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct RubyGemsResponse {
+    name: String,
+    version: String,
+    licenses: Option<Vec<String>>,
+    license: Option<String>,
+    description: Option<String>,
+    homepage_uri: Option<String>,
+    source_code_uri: Option<String>,
+    bug_tracker_uri: Option<String>,
+    documentation_uri: Option<String>,
+    mailing_list_uri: Option<String>,
+    wiki_uri: Option<String>,
+    authors: Option<String>,
+    dependencies: Option<RubyGemsDependencies>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct RubyGemsDependencies {
+    development: Option<Vec<RubyGemsDependency>>,
+    runtime: Option<Vec<RubyGemsDependency>>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct RubyGemsDependency {
+    name: String,
+    requirements: String,
+}
+
+impl From<RubyGemsResponse> for PackageMetadata {
+    fn from(response: RubyGemsResponse) -> Self {
+        let registry = RegistryInfo::new(
+            "RubyGems".to_string(),
+            "https://rubygems.org".to_string(),
+            "rubygems".to_string(),
+        );
+
+        let mut licenses = Vec::new();
+        if let Some(gem_licenses) = response.licenses {
+            licenses.extend(gem_licenses);
+        }
+        if let Some(license) = response.license {
+            if !license.trim().is_empty() && !licenses.contains(&license) {
+                licenses.push(license);
+            }
+        }
+
+        let authors = response
+            .authors
+            .map(|a| vec![a])
+            .unwrap_or_default();
+
+        PackageMetadata::new(response.name, response.version, registry)
+            .with_licenses(licenses)
+            .with_description(response.description)
+            .with_homepage(response.homepage_uri)
+            .with_repository(response.source_code_uri)
+            .with_authors(authors)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use wiremock::{Mock, MockServer, ResponseTemplate};
+    use wiremock::matchers::{method, path};
+
+    #[tokio::test]
+    async fn test_rubygems_gateway_matches() {
+        let http_client = Arc::new(HttpClient::new());
+        let gateway = RubyGemsGateway::new(http_client);
+
+        let ruby_dep = Dependency::new("rails".to_string(), "7.0.0".to_string())
+            .with_source("rubygems".to_string());
+
+        let npm_dep = Dependency::new("lodash".to_string(), "4.17.21".to_string())
+            .with_source("npm".to_string());
+
+        assert!(gateway.matches(&ruby_dep));
+        assert!(!gateway.matches(&npm_dep));
+    }
+
+    #[tokio::test]
+    async fn test_get_gem_info() {
+        let mock_server = MockServer::start().await;
+        let http_client = Arc::new(HttpClient::new());
+        let gateway = RubyGemsGateway::with_custom_url(
+            http_client,
+            mock_server.uri(),
+        );
+
+        let response_body = serde_json::json!({
+            "name": "rails",
+            "version": "7.0.0",
+            "licenses": ["MIT"],
+            "description": "Ruby on Rails",
+            "homepage_uri": "https://rubyonrails.org/"
+        });
+
+        Mock::given(method("GET"))
+            .and(path("/api/v2/rubygems/rails/versions/7.0.0.json"))
+            .respond_with(ResponseTemplate::new(200).set_body_json(&response_body))
+            .mount(&mock_server)
+            .await;
+
+        let dependency = Dependency::new("rails".to_string(), "7.0.0".to_string())
+            .with_source("rubygems".to_string());
+
+        let licenses = gateway.licenses_for(&dependency).await.unwrap();
+        assert_eq!(licenses, vec!["MIT"]);
+    }
+
+    #[tokio::test]
+    async fn test_extract_licenses_multiple_sources() {
+        let response = RubyGemsResponse {
+            name: "test-gem".to_string(),
+            version: "1.0.0".to_string(),
+            licenses: Some(vec!["MIT".to_string(), "Apache-2.0".to_string()]),
+            license: Some("BSD-3-Clause".to_string()),
+            description: None,
+            homepage_uri: None,
+            source_code_uri: None,
+            bug_tracker_uri: None,
+            documentation_uri: None,
+            mailing_list_uri: None,
+            wiki_uri: None,
+            authors: None,
+            dependencies: None,
+        };
+
+        let http_client = Arc::new(HttpClient::new());
+        let gateway = RubyGemsGateway::new(http_client);
+        let licenses = gateway.extract_licenses(&response);
+
+        // Should include all unique licenses, sorted
+        assert_eq!(licenses, vec!["Apache-2.0", "BSD-3-Clause", "MIT"]);
+    }
+
+    #[tokio::test]
+    async fn test_package_not_found() {
+        let mock_server = MockServer::start().await;
+        let http_client = Arc::new(HttpClient::new());
+        let gateway = RubyGemsGateway::with_custom_url(
+            http_client,
+            mock_server.uri(),
+        );
+
+        Mock::given(method("GET"))
+            .and(path("/api/v2/rubygems/nonexistent/versions/1.0.0.json"))
+            .respond_with(ResponseTemplate::new(404))
+            .mount(&mock_server)
+            .await;
+
+        let dependency = Dependency::new("nonexistent".to_string(), "1.0.0".to_string())
+            .with_source("rubygems".to_string());
+
+        let result = gateway.licenses_for(&dependency).await;
+        assert!(result.is_err());
+    }
+
+    #[test]
+    fn test_package_metadata_conversion() {
+        let response = RubyGemsResponse {
+            name: "rails".to_string(),
+            version: "7.0.0".to_string(),
+            licenses: Some(vec!["MIT".to_string()]),
+            license: None,
+            description: Some("Ruby on Rails web framework".to_string()),
+            homepage_uri: Some("https://rubyonrails.org/".to_string()),
+            source_code_uri: Some("https://github.com/rails/rails".to_string()),
+            bug_tracker_uri: None,
+            documentation_uri: None,
+            mailing_list_uri: None,
+            wiki_uri: None,
+            authors: Some("DHH".to_string()),
+            dependencies: None,
+        };
+
+        let metadata: PackageMetadata = response.into();
+
+        assert_eq!(metadata.name, "rails");
+        assert_eq!(metadata.version, "7.0.0");
+        assert_eq!(metadata.licenses, vec!["MIT"]);
+        assert_eq!(metadata.description, Some("Ruby on Rails web framework".to_string()));
+        assert_eq!(metadata.homepage, Some("https://rubyonrails.org/".to_string()));
+        assert_eq!(metadata.repository, Some("https://github.com/rails/rails".to_string()));
+        assert_eq!(metadata.authors, vec!["DHH"]);
+        assert_eq!(metadata.registry.name, "RubyGems");
+    }
+}
\ No newline at end of file
src/gateway/circuit.rs
@@ -0,0 +1,203 @@
+use std::collections::HashMap;
+use std::time::{Duration, Instant};
+use tracing::{debug, warn};
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum CircuitState {
+    Closed,  // Working normally
+    Open,    // Failing, requests blocked
+}
+
+#[derive(Debug, Clone)]
+pub struct CircuitBreaker {
+    state: CircuitState,
+    failure_count: u32,
+    last_failure: Option<Instant>,
+    failure_threshold: u32,
+    recovery_timeout: Duration,
+}
+
+impl CircuitBreaker {
+    pub fn new(failure_threshold: u32, recovery_timeout: Duration) -> Self {
+        Self {
+            state: CircuitState::Closed,
+            failure_count: 0,
+            last_failure: None,
+            failure_threshold,
+            recovery_timeout,
+        }
+    }
+
+    pub fn can_execute(&mut self) -> bool {
+        match self.state {
+            CircuitState::Closed => true,
+            CircuitState::Open => {
+                if let Some(last_failure) = self.last_failure {
+                    if last_failure.elapsed() >= self.recovery_timeout {
+                        debug!("Circuit breaker attempting recovery");
+                        self.state = CircuitState::Closed;
+                        self.failure_count = 0;
+                        true
+                    } else {
+                        false
+                    }
+                } else {
+                    true
+                }
+            }
+        }
+    }
+
+    pub fn record_success(&mut self) {
+        if self.state == CircuitState::Open {
+            debug!("Circuit breaker recovered - closing circuit");
+        }
+        self.state = CircuitState::Closed;
+        self.failure_count = 0;
+        self.last_failure = None;
+    }
+
+    pub fn record_failure(&mut self) {
+        self.failure_count += 1;
+        self.last_failure = Some(Instant::now());
+
+        if self.failure_count >= self.failure_threshold && self.state == CircuitState::Closed {
+            warn!(
+                "Circuit breaker opening after {} failures", 
+                self.failure_count
+            );
+            self.state = CircuitState::Open;
+        }
+    }
+
+    pub fn state(&self) -> &CircuitState {
+        &self.state
+    }
+
+    pub fn failure_count(&self) -> u32 {
+        self.failure_count
+    }
+}
+
+#[derive(Debug)]
+pub struct CircuitBreakerRegistry {
+    breakers: HashMap<String, CircuitBreaker>,
+    failure_threshold: u32,
+    recovery_timeout: Duration,
+}
+
+impl CircuitBreakerRegistry {
+    pub fn new(failure_threshold: u32, recovery_timeout: Duration) -> Self {
+        Self {
+            breakers: HashMap::new(),
+            failure_threshold,
+            recovery_timeout,
+        }
+    }
+
+    pub fn get_or_create(&mut self, host: &str) -> &mut CircuitBreaker {
+        self.breakers
+            .entry(host.to_string())
+            .or_insert_with(|| CircuitBreaker::new(self.failure_threshold, self.recovery_timeout))
+    }
+
+    pub fn can_execute(&mut self, host: &str) -> bool {
+        self.get_or_create(host).can_execute()
+    }
+
+    pub fn record_success(&mut self, host: &str) {
+        self.get_or_create(host).record_success();
+    }
+
+    pub fn record_failure(&mut self, host: &str) {
+        self.get_or_create(host).record_failure();
+    }
+}
+
+impl Default for CircuitBreakerRegistry {
+    fn default() -> Self {
+        Self::new(3, Duration::from_secs(30))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::thread;
+
+    #[test]
+    fn test_circuit_breaker_starts_closed() {
+        let mut breaker = CircuitBreaker::new(3, Duration::from_millis(100));
+        assert_eq!(breaker.state(), &CircuitState::Closed);
+        assert!(breaker.can_execute());
+    }
+
+    #[test]
+    fn test_circuit_breaker_opens_after_failures() {
+        let mut breaker = CircuitBreaker::new(3, Duration::from_millis(100));
+        
+        // Record failures
+        breaker.record_failure();
+        assert_eq!(breaker.state(), &CircuitState::Closed);
+        
+        breaker.record_failure();
+        assert_eq!(breaker.state(), &CircuitState::Closed);
+        
+        breaker.record_failure();
+        assert_eq!(breaker.state(), &CircuitState::Open);
+        assert!(!breaker.can_execute());
+    }
+
+    #[test]
+    fn test_circuit_breaker_recovery() {
+        let mut breaker = CircuitBreaker::new(2, Duration::from_millis(50));
+        
+        // Open the circuit
+        breaker.record_failure();
+        breaker.record_failure();
+        assert_eq!(breaker.state(), &CircuitState::Open);
+        
+        // Wait for recovery timeout
+        thread::sleep(Duration::from_millis(60));
+        
+        // Should allow execution after timeout
+        assert!(breaker.can_execute());
+        assert_eq!(breaker.state(), &CircuitState::Closed);
+    }
+
+    #[test]
+    fn test_circuit_breaker_success_resets() {
+        let mut breaker = CircuitBreaker::new(3, Duration::from_millis(100));
+        
+        // Record some failures
+        breaker.record_failure();
+        breaker.record_failure();
+        assert_eq!(breaker.failure_count(), 2);
+        
+        // Success should reset
+        breaker.record_success();
+        assert_eq!(breaker.failure_count(), 0);
+        assert_eq!(breaker.state(), &CircuitState::Closed);
+    }
+
+    #[test]
+    fn test_circuit_breaker_registry() {
+        let mut registry = CircuitBreakerRegistry::new(2, Duration::from_millis(100));
+        
+        // Test different hosts
+        assert!(registry.can_execute("example.com"));
+        assert!(registry.can_execute("api.example.com"));
+        
+        // Fail one host
+        registry.record_failure("example.com");
+        registry.record_failure("example.com");
+        
+        // Should block only the failed host
+        assert!(!registry.can_execute("example.com"));
+        assert!(registry.can_execute("api.example.com"));
+        
+        // Success should restore
+        registry.record_success("example.com");
+        assert!(registry.can_execute("example.com"));
+    }
+}
\ No newline at end of file
src/gateway/http.rs
@@ -0,0 +1,322 @@
+use crate::gateway::circuit::CircuitBreakerRegistry;
+use anyhow::Result;
+use reqwest::{Client, Response};
+use std::sync::Mutex;
+use std::time::Duration;
+use tracing::{debug, warn};
+use url::Url;
+
+#[derive(Debug)]
+pub struct HttpClient {
+    client: Client,
+    circuit_breakers: Mutex<CircuitBreakerRegistry>,
+    retry_count: u32,
+    open_timeout: Duration,
+    read_timeout: Duration,
+}
+
+impl HttpClient {
+    pub fn new() -> Self {
+        let client = Client::builder()
+            .timeout(Duration::from_secs(10))
+            .connect_timeout(Duration::from_secs(5))
+            .user_agent("spandx-rs/1.0.0")
+            .build()
+            .expect("Failed to create HTTP client");
+
+        Self {
+            client,
+            circuit_breakers: Mutex::new(CircuitBreakerRegistry::default()),
+            retry_count: 3,
+            open_timeout: Duration::from_secs(1),
+            read_timeout: Duration::from_secs(5),
+        }
+    }
+
+    pub fn with_timeouts(mut self, open_timeout: Duration, read_timeout: Duration) -> Self {
+        self.open_timeout = open_timeout;
+        self.read_timeout = read_timeout;
+        
+        // Recreate client with new timeouts
+        let client = Client::builder()
+            .timeout(read_timeout)
+            .connect_timeout(open_timeout)
+            .user_agent("spandx-rs/1.0.0")
+            .build()
+            .expect("Failed to create HTTP client with custom timeouts");
+        
+        self.client = client;
+        self
+    }
+
+    pub fn with_retry_count(mut self, retry_count: u32) -> Self {
+        self.retry_count = retry_count;
+        self
+    }
+
+    pub async fn get(&self, url: &str) -> Result<Response> {
+        self.get_with_retries(url, false).await
+    }
+
+    pub async fn get_escaped(&self, url: &str) -> Result<Response> {
+        self.get_with_retries(url, true).await
+    }
+
+    async fn get_with_retries(&self, url: &str, escape: bool) -> Result<Response> {
+        if crate::is_airgap_mode() {
+            return Err(anyhow::anyhow!("HTTP requests disabled in airgap mode"));
+        }
+
+        let final_url = if escape {
+            self.escape_url(url)?
+        } else {
+            url.to_string()
+        };
+
+        let parsed_url = Url::parse(&final_url)?;
+        let host = parsed_url.host_str().unwrap_or("unknown");
+
+        // Check circuit breaker
+        {
+            let mut breakers = self.circuit_breakers.lock().unwrap();
+            if !breakers.can_execute(host) {
+                return Err(anyhow::anyhow!("Circuit breaker open for host: {}", host));
+            }
+        }
+
+        let mut last_error = None;
+        
+        for attempt in 1..=self.retry_count {
+            debug!("HTTP GET attempt {} for {}", attempt, final_url);
+            
+            match self.client.get(&final_url).send().await {
+                Ok(response) => {
+                    if response.status().is_success() {
+                        // Record success in circuit breaker
+                        {
+                            let mut breakers = self.circuit_breakers.lock().unwrap();
+                            breakers.record_success(host);
+                        }
+                        return Ok(response);
+                    } else {
+                        let status = response.status();
+                        warn!("HTTP request failed with status {}: {}", status, final_url);
+                        
+                        // Don't retry on client errors (4xx)
+                        if status.is_client_error() {
+                            return Err(anyhow::anyhow!("Client error: {}", status));
+                        }
+                        
+                        last_error = Some(anyhow::anyhow!("Server error: {}", status));
+                    }
+                }
+                Err(e) => {
+                    warn!("HTTP request error on attempt {}: {}", attempt, e);
+                    last_error = Some(anyhow::anyhow!("Request error: {}", e));
+                    
+                    // Record failure in circuit breaker on final attempt
+                    if attempt == self.retry_count {
+                        let mut breakers = self.circuit_breakers.lock().unwrap();
+                        breakers.record_failure(host);
+                    }
+                }
+            }
+
+            // Exponential backoff between retries
+            if attempt < self.retry_count {
+                let delay = Duration::from_millis(100 * 2_u64.pow(attempt - 1));
+                tokio::time::sleep(delay).await;
+            }
+        }
+
+        // Record failure in circuit breaker
+        {
+            let mut breakers = self.circuit_breakers.lock().unwrap();
+            breakers.record_failure(host);
+        }
+
+        Err(last_error.unwrap_or_else(|| anyhow::anyhow!("All retries failed")))
+    }
+
+    pub fn ok(&self, response: &Response) -> bool {
+        response.status().is_success()
+    }
+
+    fn escape_url(&self, url: &str) -> Result<String> {
+        // Simple URL escaping - encode only the path components
+        let parsed = Url::parse(url)?;
+        let mut escaped = String::new();
+        
+        escaped.push_str(&format!("{}://", parsed.scheme()));
+        
+        if let Some(host) = parsed.host_str() {
+            escaped.push_str(host);
+        }
+        
+        if let Some(port) = parsed.port() {
+            escaped.push_str(&format!(":{}", port));
+        }
+        
+        // Encode path segments
+        for segment in parsed.path_segments().unwrap_or("".split('/')) {
+            if !segment.is_empty() {
+                escaped.push('/');
+                escaped.push_str(&urlencoding::encode(segment));
+            }
+        }
+        
+        if let Some(query) = parsed.query() {
+            escaped.push('?');
+            escaped.push_str(query);
+        }
+        
+        if let Some(fragment) = parsed.fragment() {
+            escaped.push('#');
+            escaped.push_str(fragment);
+        }
+        
+        Ok(escaped)
+    }
+
+    pub async fn get_json<T>(&self, url: &str) -> Result<T>
+    where
+        T: serde::de::DeserializeOwned,
+    {
+        let response = self.get(url).await?;
+        let text = response.text().await?;
+        let parsed: T = serde_json::from_str(&text)?;
+        Ok(parsed)
+    }
+
+    pub async fn get_text(&self, url: &str) -> Result<String> {
+        let response = self.get(url).await?;
+        Ok(response.text().await?)
+    }
+
+    pub async fn get_bytes(&self, url: &str) -> Result<Vec<u8>> {
+        let response = self.get(url).await?;
+        Ok(response.bytes().await?.to_vec())
+    }
+}
+
+impl Default for HttpClient {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use wiremock::{Mock, MockServer, ResponseTemplate};
+    use wiremock::matchers::{method, path};
+
+    #[tokio::test]
+    async fn test_successful_get_request() {
+        let mock_server = MockServer::start().await;
+        
+        Mock::given(method("GET"))
+            .and(path("/test"))
+            .respond_with(ResponseTemplate::new(200).set_body_string("success"))
+            .mount(&mock_server)
+            .await;
+
+        let client = HttpClient::new();
+        let url = format!("{}/test", mock_server.uri());
+        let response = client.get(&url).await.unwrap();
+        
+        assert!(client.ok(&response));
+        assert_eq!(response.text().await.unwrap(), "success");
+    }
+
+    #[tokio::test]
+    async fn test_get_json() {
+        let mock_server = MockServer::start().await;
+        
+        Mock::given(method("GET"))
+            .and(path("/json"))
+            .respond_with(ResponseTemplate::new(200).set_body_json(&serde_json::json!({
+                "name": "test",
+                "version": "1.0.0"
+            })))
+            .mount(&mock_server)
+            .await;
+
+        let client = HttpClient::new();
+        let url = format!("{}/json", mock_server.uri());
+        
+        let result: serde_json::Value = client.get_json(&url).await.unwrap();
+        assert_eq!(result["name"], "test");
+        assert_eq!(result["version"], "1.0.0");
+    }
+
+    #[tokio::test]
+    async fn test_retry_on_server_error() {
+        let mock_server = MockServer::start().await;
+        
+        // First two requests fail, third succeeds
+        Mock::given(method("GET"))
+            .and(path("/retry"))
+            .respond_with(ResponseTemplate::new(500))
+            .up_to_n_times(2)
+            .mount(&mock_server)
+            .await;
+            
+        Mock::given(method("GET"))
+            .and(path("/retry"))
+            .respond_with(ResponseTemplate::new(200).set_body_string("success"))
+            .mount(&mock_server)
+            .await;
+
+        let client = HttpClient::new().with_retry_count(3);
+        let url = format!("{}/retry", mock_server.uri());
+        let response = client.get(&url).await.unwrap();
+        
+        assert!(client.ok(&response));
+    }
+
+    #[tokio::test]
+    async fn test_no_retry_on_client_error() {
+        let mock_server = MockServer::start().await;
+        
+        Mock::given(method("GET"))
+            .and(path("/client-error"))
+            .respond_with(ResponseTemplate::new(404))
+            .mount(&mock_server)
+            .await;
+
+        let client = HttpClient::new().with_retry_count(3);
+        let url = format!("{}/client-error", mock_server.uri());
+        let result = client.get(&url).await;
+        
+        assert!(result.is_err());
+        assert!(result.unwrap_err().to_string().contains("Client error"));
+    }
+
+    #[test]
+    fn test_url_escaping() {
+        let client = HttpClient::new();
+        
+        let url = "https://example.com/path with spaces/file.json";
+        let escaped = client.escape_url(url).unwrap();
+        assert_eq!(escaped, "https://example.com/path%20with%20spaces/file.json");
+        
+        let url_with_query = "https://example.com/path?query=test value";
+        let escaped = client.escape_url(url_with_query).unwrap();
+        assert_eq!(escaped, "https://example.com/path?query=test value");
+    }
+
+    #[test]
+    fn test_airgap_mode() {
+        crate::set_airgap_mode(true);
+        
+        let rt = tokio::runtime::Runtime::new().unwrap();
+        let client = HttpClient::new();
+        
+        let result = rt.block_on(client.get("https://example.com"));
+        assert!(result.is_err());
+        assert!(result.unwrap_err().to_string().contains("airgap mode"));
+        
+        crate::set_airgap_mode(false);
+    }
+}
\ No newline at end of file
src/gateway/mod.rs
@@ -0,0 +1,10 @@
+pub mod http;
+pub mod circuit;
+pub mod traits;
+pub mod registry;
+pub mod registries;
+
+pub use http::HttpClient;
+pub use circuit::{CircuitBreaker, CircuitState};
+pub use traits::Gateway;
+pub use registry::GatewayRegistry;
\ No newline at end of file
src/gateway/registry.rs
@@ -0,0 +1,336 @@
+use crate::core::Dependency;
+use crate::gateway::traits::{Gateway, GatewayError, GatewayResult};
+use crate::gateway::HttpClient;
+use std::sync::Arc;
+use tracing::{debug, warn};
+
+/// Registry for managing and discovering package registry gateways
+#[derive(Debug)]
+pub struct GatewayRegistry {
+    gateways: Vec<Box<dyn Gateway>>,
+    http_client: Arc<HttpClient>,
+}
+
+impl GatewayRegistry {
+    pub fn new(http_client: Arc<HttpClient>) -> Self {
+        Self {
+            gateways: Vec::new(),
+            http_client,
+        }
+    }
+
+    /// Register a new gateway
+    pub fn register<G>(&mut self, gateway: G)
+    where
+        G: Gateway + 'static,
+    {
+        debug!("Registering gateway: {}", gateway.name());
+        self.gateways.push(Box::new(gateway));
+    }
+
+    /// Find the first gateway that matches the given dependency
+    pub fn find_gateway(&self, dependency: &Dependency) -> Option<&dyn Gateway> {
+        for gateway in &self.gateways {
+            if gateway.matches(dependency) {
+                debug!(
+                    "Found matching gateway '{}' for dependency {}@{}",
+                    gateway.name(),
+                    dependency.name,
+                    dependency.version
+                );
+                return Some(gateway.as_ref());
+            }
+        }
+        
+        debug!(
+            "No gateway found for dependency {}@{} (source: {:?})",
+            dependency.name, dependency.version, dependency.source
+        );
+        None
+    }
+
+    /// Get license information for a dependency using the appropriate gateway
+    pub async fn get_licenses(&self, dependency: &Dependency) -> GatewayResult<Vec<String>> {
+        if let Some(gateway) = self.find_gateway(dependency) {
+            debug!(
+                "Fetching licenses for {}@{} using gateway '{}'",
+                dependency.name, dependency.version, gateway.name()
+            );
+            
+            match gateway.licenses_for(dependency).await {
+                Ok(licenses) => {
+                    debug!(
+                        "Found {} licenses for {}@{}: {:?}",
+                        licenses.len(), dependency.name, dependency.version, licenses
+                    );
+                    Ok(licenses)
+                }
+                Err(e) => {
+                    warn!(
+                        "Failed to get licenses for {}@{} from gateway '{}': {}",
+                        dependency.name, dependency.version, gateway.name(), e
+                    );
+                    Err(GatewayError::Registry {
+                        message: format!("Gateway '{}' failed: {}", gateway.name(), e),
+                    })
+                }
+            }
+        } else {
+            Ok(Vec::new()) // Return empty list if no gateway found
+        }
+    }
+
+    /// Get all registered gateways
+    pub fn gateways(&self) -> &[Box<dyn Gateway>] {
+        &self.gateways
+    }
+
+    /// Get the number of registered gateways
+    pub fn len(&self) -> usize {
+        self.gateways.len()
+    }
+
+    /// Check if there are any registered gateways
+    pub fn is_empty(&self) -> bool {
+        self.gateways.is_empty()
+    }
+
+    /// Get a reference to the HTTP client
+    pub fn http_client(&self) -> Arc<HttpClient> {
+        Arc::clone(&self.http_client)
+    }
+
+    /// Get license information for multiple dependencies concurrently
+    pub async fn get_licenses_batch(
+        &self,
+        dependencies: &[Dependency],
+    ) -> Vec<(Dependency, GatewayResult<Vec<String>>)> {
+        let futures = dependencies.iter().map(|dep| async {
+            let result = self.get_licenses(dep).await;
+            (dep.clone(), result)
+        });
+
+        futures::future::join_all(futures).await
+    }
+
+    /// List all supported package managers from registered gateways
+    pub fn supported_package_managers(&self) -> Vec<String> {
+        // This would typically be implemented by asking each gateway
+        // what package managers it supports. For now, return common ones.
+        vec![
+            "rubygems".to_string(),
+            "npm".to_string(),
+            "yarn".to_string(),
+            "pypi".to_string(),
+            "nuget".to_string(),
+            "maven".to_string(),
+            "packagist".to_string(),
+        ]
+    }
+}
+
+impl Default for GatewayRegistry {
+    fn default() -> Self {
+        Self::new(Arc::new(HttpClient::new()))
+    }
+}
+
+/// Builder for constructing a gateway registry with common gateways
+pub struct GatewayRegistryBuilder {
+    registry: GatewayRegistry,
+}
+
+impl GatewayRegistryBuilder {
+    pub fn new(http_client: Arc<HttpClient>) -> Self {
+        Self {
+            registry: GatewayRegistry::new(http_client),
+        }
+    }
+
+    pub fn with_rubygems(self) -> Self {
+        // Would register RubyGems gateway here
+        // self.registry.register(RubyGemsGateway::new(self.registry.http_client()));
+        self
+    }
+
+    pub fn with_npm(self) -> Self {
+        // Would register NPM gateway here
+        // self.registry.register(NpmGateway::new(self.registry.http_client()));
+        self
+    }
+
+    pub fn with_pypi(self) -> Self {
+        // Would register PyPI gateway here
+        // self.registry.register(PypiGateway::new(self.registry.http_client()));
+        self
+    }
+
+    pub fn with_nuget(self) -> Self {
+        // Would register NuGet gateway here
+        // self.registry.register(NugetGateway::new(self.registry.http_client()));
+        self
+    }
+
+    pub fn with_maven(self) -> Self {
+        // Would register Maven gateway here
+        // self.registry.register(MavenGateway::new(self.registry.http_client()));
+        self
+    }
+
+    pub fn with_packagist(self) -> Self {
+        // Would register Packagist gateway here
+        // self.registry.register(PackagistGateway::new(self.registry.http_client()));
+        self
+    }
+
+    pub fn with_all_default_gateways(self) -> Self {
+        self.with_rubygems()
+            .with_npm()
+            .with_pypi()
+            .with_nuget()
+            .with_maven()
+            .with_packagist()
+    }
+
+    pub fn build(self) -> GatewayRegistry {
+        self.registry
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::core::PackageManager;
+    use async_trait::async_trait;
+
+    // Mock gateway for testing
+    #[derive(Debug)]
+    struct MockGateway {
+        name: &'static str,
+        package_manager: PackageManager,
+        licenses: Vec<String>,
+    }
+
+    impl MockGateway {
+        fn new(name: &'static str, package_manager: PackageManager, licenses: Vec<String>) -> Self {
+            Self {
+                name,
+                package_manager,
+                licenses,
+            }
+        }
+    }
+
+    #[async_trait]
+    impl Gateway for MockGateway {
+        fn matches(&self, dependency: &Dependency) -> bool {
+            if let Some(source) = &dependency.source {
+                PackageManager::from_source(source) == self.package_manager
+            } else {
+                false
+            }
+        }
+
+        async fn licenses_for(&self, _dependency: &Dependency) -> anyhow::Result<Vec<String>> {
+            Ok(self.licenses.clone())
+        }
+
+        fn name(&self) -> &'static str {
+            self.name
+        }
+
+        fn base_url(&self) -> &str {
+            "https://mock.example.com"
+        }
+    }
+
+    #[tokio::test]
+    async fn test_gateway_registry_registration() {
+        let http_client = Arc::new(HttpClient::new());
+        let mut registry = GatewayRegistry::new(http_client);
+        
+        let gateway = MockGateway::new("MockRubyGems", PackageManager::RubyGems, vec!["MIT".to_string()]);
+        registry.register(gateway);
+        
+        assert_eq!(registry.len(), 1);
+        assert!(!registry.is_empty());
+    }
+
+    #[tokio::test]
+    async fn test_gateway_matching() {
+        let http_client = Arc::new(HttpClient::new());
+        let mut registry = GatewayRegistry::new(http_client);
+        
+        let rubygems_gateway = MockGateway::new(
+            "MockRubyGems",
+            PackageManager::RubyGems,
+            vec!["MIT".to_string()],
+        );
+        let npm_gateway = MockGateway::new(
+            "MockNPM",
+            PackageManager::Npm,
+            vec!["Apache-2.0".to_string()],
+        );
+        
+        registry.register(rubygems_gateway);
+        registry.register(npm_gateway);
+        
+        let ruby_dep = Dependency::new("rails".to_string(), "7.0.0".to_string())
+            .with_source("rubygems".to_string());
+        
+        let npm_dep = Dependency::new("lodash".to_string(), "4.17.21".to_string())
+            .with_source("npm".to_string());
+        
+        let ruby_gateway = registry.find_gateway(&ruby_dep);
+        assert!(ruby_gateway.is_some());
+        assert_eq!(ruby_gateway.unwrap().name(), "MockRubyGems");
+        
+        let npm_gateway = registry.find_gateway(&npm_dep);
+        assert!(npm_gateway.is_some());
+        assert_eq!(npm_gateway.unwrap().name(), "MockNPM");
+    }
+
+    #[tokio::test]
+    async fn test_get_licenses() {
+        let http_client = Arc::new(HttpClient::new());
+        let mut registry = GatewayRegistry::new(http_client);
+        
+        let gateway = MockGateway::new(
+            "MockRubyGems",
+            PackageManager::RubyGems,
+            vec!["MIT".to_string(), "Apache-2.0".to_string()],
+        );
+        registry.register(gateway);
+        
+        let dependency = Dependency::new("rails".to_string(), "7.0.0".to_string())
+            .with_source("rubygems".to_string());
+        
+        let licenses = registry.get_licenses(&dependency).await.unwrap();
+        assert_eq!(licenses, vec!["MIT", "Apache-2.0"]);
+    }
+
+    #[tokio::test]
+    async fn test_no_matching_gateway() {
+        let http_client = Arc::new(HttpClient::new());
+        let registry = GatewayRegistry::new(http_client);
+        
+        let dependency = Dependency::new("unknown".to_string(), "1.0.0".to_string())
+            .with_source("unknown_pm".to_string());
+        
+        let licenses = registry.get_licenses(&dependency).await.unwrap();
+        assert!(licenses.is_empty());
+    }
+
+    #[test]
+    fn test_builder_pattern() {
+        let http_client = Arc::new(HttpClient::new());
+        let registry = GatewayRegistryBuilder::new(http_client)
+            .with_rubygems()
+            .with_npm()
+            .build();
+        
+        // Registry is built but no actual gateways are registered in this test
+        // because the actual gateway implementations are not available
+        assert_eq!(registry.len(), 0);
+    }
+}
\ No newline at end of file
src/gateway/traits.rs
@@ -0,0 +1,205 @@
+use crate::core::Dependency;
+use anyhow::Result;
+use async_trait::async_trait;
+use std::fmt::Debug;
+
+/// Gateway trait for fetching license information from package registries
+#[async_trait]
+pub trait Gateway: Send + Sync + Debug {
+    /// Check if this gateway can handle the given dependency
+    fn matches(&self, dependency: &Dependency) -> bool;
+    
+    /// Fetch license information for the given dependency
+    async fn licenses_for(&self, dependency: &Dependency) -> Result<Vec<String>>;
+    
+    /// Get the name of this gateway (for logging/debugging)
+    fn name(&self) -> &'static str;
+    
+    /// Get the base URL of the registry this gateway connects to
+    fn base_url(&self) -> &str;
+}
+
+/// Registry information for package sources
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RegistryInfo {
+    pub name: String,
+    pub url: String,
+    pub package_manager: String,
+}
+
+impl RegistryInfo {
+    pub fn new(name: String, url: String, package_manager: String) -> Self {
+        Self {
+            name,
+            url,
+            package_manager,
+        }
+    }
+}
+
+/// Result type for gateway operations
+pub type GatewayResult<T> = Result<T, GatewayError>;
+
+/// Errors that can occur during gateway operations
+#[derive(Debug, thiserror::Error)]
+pub enum GatewayError {
+    #[error("HTTP request failed: {0}")]
+    Http(#[from] reqwest::Error),
+    
+    #[error("JSON parsing failed: {0}")]
+    Json(#[from] serde_json::Error),
+    
+    #[error("XML parsing failed: {0}")]
+    Xml(String),
+    
+    #[error("URL parsing failed: {0}")]
+    Url(#[from] url::ParseError),
+    
+    #[error("Circuit breaker open for host: {host}")]
+    CircuitBreakerOpen { host: String },
+    
+    #[error("Package not found: {name}@{version}")]
+    PackageNotFound { name: String, version: String },
+    
+    #[error("Registry error: {message}")]
+    Registry { message: String },
+    
+    #[error("Authentication failed for registry: {registry}")]
+    Authentication { registry: String },
+    
+    #[error("Rate limit exceeded for registry: {registry}")]
+    RateLimit { registry: String },
+    
+    #[error("Airgap mode enabled - network requests disabled")]
+    AirgapMode,
+    
+    #[error("Operation timed out")]
+    Timeout,
+    
+    #[error("IO error: {0}")]
+    Io(#[from] std::io::Error),
+}
+
+/// Metadata about a package from a registry
+#[derive(Debug, Clone, PartialEq)]
+pub struct PackageMetadata {
+    pub name: String,
+    pub version: String,
+    pub licenses: Vec<String>,
+    pub description: Option<String>,
+    pub homepage: Option<String>,
+    pub repository: Option<String>,
+    pub authors: Vec<String>,
+    pub dependencies: Vec<String>,
+    pub registry: RegistryInfo,
+}
+
+impl PackageMetadata {
+    pub fn new(name: String, version: String, registry: RegistryInfo) -> Self {
+        Self {
+            name,
+            version,
+            licenses: Vec::new(),
+            description: None,
+            homepage: None,
+            repository: None,
+            authors: Vec::new(),
+            dependencies: Vec::new(),
+            registry,
+        }
+    }
+    
+    pub fn with_licenses(mut self, licenses: Vec<String>) -> Self {
+        self.licenses = licenses;
+        self
+    }
+    
+    pub fn with_description(mut self, description: Option<String>) -> Self {
+        self.description = description;
+        self
+    }
+    
+    pub fn with_homepage(mut self, homepage: Option<String>) -> Self {
+        self.homepage = homepage;
+        self
+    }
+    
+    pub fn with_repository(mut self, repository: Option<String>) -> Self {
+        self.repository = repository;
+        self
+    }
+    
+    pub fn with_authors(mut self, authors: Vec<String>) -> Self {
+        self.authors = authors;
+        self
+    }
+    
+    pub fn with_dependencies(mut self, dependencies: Vec<String>) -> Self {
+        self.dependencies = dependencies;
+        self
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_registry_info_creation() {
+        let registry = RegistryInfo::new(
+            "RubyGems".to_string(),
+            "https://rubygems.org".to_string(),
+            "rubygems".to_string(),
+        );
+        
+        assert_eq!(registry.name, "RubyGems");
+        assert_eq!(registry.url, "https://rubygems.org");
+        assert_eq!(registry.package_manager, "rubygems");
+    }
+
+    #[test]
+    fn test_package_metadata_builder() {
+        let registry = RegistryInfo::new(
+            "NPM".to_string(),
+            "https://registry.npmjs.org".to_string(),
+            "npm".to_string(),
+        );
+        
+        let metadata = PackageMetadata::new(
+            "lodash".to_string(),
+            "4.17.21".to_string(),
+            registry.clone(),
+        )
+        .with_licenses(vec!["MIT".to_string()])
+        .with_description(Some("Lodash modular utilities.".to_string()))
+        .with_homepage(Some("https://lodash.com/".to_string()));
+        
+        assert_eq!(metadata.name, "lodash");
+        assert_eq!(metadata.version, "4.17.21");
+        assert_eq!(metadata.licenses, vec!["MIT"]);
+        assert_eq!(metadata.description, Some("Lodash modular utilities.".to_string()));
+        assert_eq!(metadata.registry, registry);
+    }
+
+    #[test]
+    fn test_gateway_error_display() {
+        let error = GatewayError::PackageNotFound {
+            name: "nonexistent".to_string(),
+            version: "1.0.0".to_string(),
+        };
+        
+        assert_eq!(
+            error.to_string(),
+            "Package not found: nonexistent@1.0.0"
+        );
+        
+        let error = GatewayError::CircuitBreakerOpen {
+            host: "api.example.com".to_string(),
+        };
+        
+        assert_eq!(
+            error.to_string(),
+            "Circuit breaker open for host: api.example.com"
+        );
+    }
+}
\ No newline at end of file
src/git/config.rs
@@ -0,0 +1,351 @@
+use anyhow::Result;
+use camino::Utf8PathBuf;
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+
+/// Configuration for Git repositories
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GitConfig {
+    pub repositories: HashMap<String, RepositoryConfig>,
+    pub base_path: Option<Utf8PathBuf>,
+    pub default_branch: String,
+    pub shallow_clone: bool,
+    pub fetch_depth: i32,
+}
+
+impl Default for GitConfig {
+    fn default() -> Self {
+        let mut repositories = HashMap::new();
+        
+        repositories.insert(
+            "cache".to_string(),
+            RepositoryConfig {
+                url: "https://github.com/spandx/cache.git".to_string(),
+                branch: Some("main".to_string()),
+                enabled: true,
+                description: Some("Pre-computed license cache".to_string()),
+            },
+        );
+        
+        repositories.insert(
+            "rubygems".to_string(),
+            RepositoryConfig {
+                url: "https://github.com/spandx/rubygems-cache.git".to_string(),
+                branch: Some("main".to_string()),
+                enabled: true,
+                description: Some("RubyGems specific license cache".to_string()),
+            },
+        );
+        
+        repositories.insert(
+            "spdx".to_string(),
+            RepositoryConfig {
+                url: "https://github.com/spdx/license-list-data.git".to_string(),
+                branch: Some("main".to_string()),
+                enabled: true,
+                description: Some("SPDX license list data".to_string()),
+            },
+        );
+        
+        Self {
+            repositories,
+            base_path: None,
+            default_branch: "main".to_string(),
+            shallow_clone: true,
+            fetch_depth: 1,
+        }
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RepositoryConfig {
+    pub url: String,
+    pub branch: Option<String>,
+    pub enabled: bool,
+    pub description: Option<String>,
+}
+
+impl GitConfig {
+    /// Load configuration from file
+    pub async fn load_from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
+        let content = tokio::fs::read_to_string(path).await?;
+        let config: GitConfig = toml::from_str(&content)?;
+        Ok(config)
+    }
+    
+    /// Save configuration to file
+    pub async fn save_to_file<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
+        let content = toml::to_string_pretty(self)?;
+        
+        // Ensure parent directory exists
+        if let Some(parent) = path.as_ref().parent() {
+            tokio::fs::create_dir_all(parent).await?;
+        }
+        
+        tokio::fs::write(path, content).await?;
+        Ok(())
+    }
+    
+    /// Get base path for repositories
+    pub fn get_base_path(&self) -> Result<Utf8PathBuf> {
+        if let Some(base_path) = &self.base_path {
+            Ok(base_path.clone())
+        } else {
+            // Default to ~/.local/share/spandx
+            let home_dir = dirs::home_dir()
+                .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
+            
+            let base_path = Utf8PathBuf::from_path_buf(home_dir)
+                .map_err(|_| anyhow::anyhow!("Invalid UTF-8 in home directory path"))?
+                .join(".local")
+                .join("share")
+                .join("spandx");
+            
+            Ok(base_path)
+        }
+    }
+    
+    /// Get local path for a repository
+    pub fn get_repository_path(&self, repo_name: &str) -> Result<Utf8PathBuf> {
+        let base_path = self.get_base_path()?;
+        Ok(base_path.join(repo_name))
+    }
+    
+    /// Get branch for a repository (with fallback to default)
+    pub fn get_repository_branch(&self, repo_name: &str) -> String {
+        self.repositories
+            .get(repo_name)
+            .and_then(|config| config.branch.as_ref())
+            .unwrap_or(&self.default_branch)
+            .clone()
+    }
+    
+    /// Check if a repository is enabled
+    pub fn is_repository_enabled(&self, repo_name: &str) -> bool {
+        self.repositories
+            .get(repo_name)
+            .map(|config| config.enabled)
+            .unwrap_or(false)
+    }
+    
+    /// Get all enabled repositories
+    pub fn get_enabled_repositories(&self) -> Vec<String> {
+        self.repositories
+            .iter()
+            .filter(|(_, config)| config.enabled)
+            .map(|(name, _)| name.clone())
+            .collect()
+    }
+    
+    /// Add or update a repository
+    pub fn add_repository(&mut self, name: String, config: RepositoryConfig) {
+        self.repositories.insert(name, config);
+    }
+    
+    /// Remove a repository
+    pub fn remove_repository(&mut self, name: &str) -> Option<RepositoryConfig> {
+        self.repositories.remove(name)
+    }
+    
+    /// Enable or disable a repository
+    pub fn set_repository_enabled(&mut self, name: &str, enabled: bool) -> Result<()> {
+        if let Some(config) = self.repositories.get_mut(name) {
+            config.enabled = enabled;
+            Ok(())
+        } else {
+            Err(anyhow::anyhow!("Repository not found: {}", name))
+        }
+    }
+    
+    /// Validate configuration
+    pub fn validate(&self) -> Result<()> {
+        // Check that base path is valid if specified
+        if let Some(base_path) = &self.base_path {
+            if !base_path.is_absolute() {
+                return Err(anyhow::anyhow!("Base path must be absolute: {}", base_path));
+            }
+        }
+        
+        // Validate repository URLs
+        for (name, config) in &self.repositories {
+            if config.url.is_empty() {
+                return Err(anyhow::anyhow!("Repository {} has empty URL", name));
+            }
+            
+            // Basic URL validation
+            if !config.url.starts_with("http://") && !config.url.starts_with("https://") {
+                return Err(anyhow::anyhow!("Repository {} has invalid URL: {}", name, config.url));
+            }
+        }
+        
+        // Check fetch depth
+        if self.fetch_depth <= 0 {
+            return Err(anyhow::anyhow!("Fetch depth must be positive: {}", self.fetch_depth));
+        }
+        
+        Ok(())
+    }
+    
+    /// Create repositories from this configuration
+    pub fn create_repositories(&self) -> Result<HashMap<String, crate::git::GitRepository>> {
+        self.validate()?;
+        
+        let mut repositories = HashMap::new();
+        
+        for (name, config) in &self.repositories {
+            if !config.enabled {
+                continue;
+            }
+            
+            let local_path = self.get_repository_path(name)?;
+            let branch = self.get_repository_branch(name);
+            
+            let repo = crate::git::GitRepository::new(
+                config.url.clone(),
+                branch,
+                local_path,
+            );
+            
+            repositories.insert(name.clone(), repo);
+        }
+        
+        Ok(repositories)
+    }
+}
+
+/// Get default configuration file path
+pub fn get_default_config_path() -> Result<Utf8PathBuf> {
+    let home_dir = dirs::home_dir()
+        .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
+    
+    let config_path = Utf8PathBuf::from_path_buf(home_dir)
+        .map_err(|_| anyhow::anyhow!("Invalid UTF-8 in home directory path"))?
+        .join(".config")
+        .join("spandx")
+        .join("git.toml");
+    
+    Ok(config_path)
+}
+
+/// Load configuration with fallback to defaults
+pub async fn load_config_with_defaults() -> Result<GitConfig> {
+    let config_path = get_default_config_path()?;
+    
+    if config_path.exists() {
+        match GitConfig::load_from_file(&config_path).await {
+            Ok(config) => {
+                config.validate()?;
+                Ok(config)
+            }
+            Err(e) => {
+                tracing::warn!("Failed to load Git config from {:?}: {}", config_path, e);
+                tracing::info!("Using default configuration");
+                Ok(GitConfig::default())
+            }
+        }
+    } else {
+        tracing::debug!("No Git config file found at {:?}, using defaults", config_path);
+        Ok(GitConfig::default())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use tempfile::TempDir;
+    
+    #[test]
+    fn test_default_config() {
+        let config = GitConfig::default();
+        
+        assert_eq!(config.default_branch, "main");
+        assert!(config.shallow_clone);
+        assert_eq!(config.fetch_depth, 1);
+        assert_eq!(config.repositories.len(), 3);
+        
+        // Check that required repositories exist
+        assert!(config.repositories.contains_key("cache"));
+        assert!(config.repositories.contains_key("rubygems"));
+        assert!(config.repositories.contains_key("spdx"));
+        
+        // Check that all are enabled by default
+        assert!(config.is_repository_enabled("cache"));
+        assert!(config.is_repository_enabled("rubygems"));
+        assert!(config.is_repository_enabled("spdx"));
+    }
+    
+    #[test]
+    fn test_repository_management() {
+        let mut config = GitConfig::default();
+        
+        // Test adding a repository
+        config.add_repository(
+            "test".to_string(),
+            RepositoryConfig {
+                url: "https://github.com/test/repo.git".to_string(),
+                branch: Some("develop".to_string()),
+                enabled: true,
+                description: Some("Test repository".to_string()),
+            },
+        );
+        
+        assert!(config.repositories.contains_key("test"));
+        assert!(config.is_repository_enabled("test"));
+        assert_eq!(config.get_repository_branch("test"), "develop");
+        
+        // Test disabling a repository
+        config.set_repository_enabled("test", false).unwrap();
+        assert!(!config.is_repository_enabled("test"));
+        
+        // Test removing a repository
+        let removed = config.remove_repository("test");
+        assert!(removed.is_some());
+        assert!(!config.repositories.contains_key("test"));
+    }
+    
+    #[tokio::test]
+    async fn test_config_file_operations() {
+        let temp_dir = TempDir::new().unwrap();
+        let config_path = temp_dir.path().join("git.toml");
+        
+        let original_config = GitConfig::default();
+        
+        // Save config
+        original_config.save_to_file(&config_path).await.unwrap();
+        assert!(config_path.exists());
+        
+        // Load config
+        let loaded_config = GitConfig::load_from_file(&config_path).await.unwrap();
+        
+        // Compare key fields
+        assert_eq!(loaded_config.default_branch, original_config.default_branch);
+        assert_eq!(loaded_config.shallow_clone, original_config.shallow_clone);
+        assert_eq!(loaded_config.fetch_depth, original_config.fetch_depth);
+        assert_eq!(loaded_config.repositories.len(), original_config.repositories.len());
+    }
+    
+    #[test]
+    fn test_config_validation() {
+        let mut config = GitConfig::default();
+        
+        // Valid config should pass
+        assert!(config.validate().is_ok());
+        
+        // Invalid fetch depth should fail
+        config.fetch_depth = 0;
+        assert!(config.validate().is_err());
+        config.fetch_depth = 1;
+        
+        // Invalid URL should fail
+        config.add_repository(
+            "invalid".to_string(),
+            RepositoryConfig {
+                url: "not-a-url".to_string(),
+                branch: None,
+                enabled: true,
+                description: None,
+            },
+        );
+        assert!(config.validate().is_err());
+    }
+}
\ No newline at end of file
src/git/mod.rs
@@ -0,0 +1,103 @@
+pub mod repository;
+pub mod operations;
+pub mod config;
+
+pub use repository::GitRepository;
+pub use operations::GitOperations;
+pub use config::GitConfig;
+
+use anyhow::Result;
+use camino::Utf8PathBuf;
+use std::collections::HashMap;
+
+/// Initialize default Git repositories for Spandx
+pub fn init_default_repositories() -> Result<HashMap<String, GitRepository>> {
+    let mut repos = HashMap::new();
+    
+    // Cache repository for pre-computed license data
+    repos.insert(
+        "cache".to_string(),
+        GitRepository::new(
+            "https://github.com/spandx/cache.git".to_string(),
+            "main".to_string(),
+            get_local_path("cache")?,
+        ),
+    );
+    
+    // RubyGems-specific cache repository
+    repos.insert(
+        "rubygems".to_string(),
+        GitRepository::new(
+            "https://github.com/spandx/rubygems-cache.git".to_string(),
+            "main".to_string(),
+            get_local_path("rubygems-cache")?,
+        ),
+    );
+    
+    // SPDX license list data
+    repos.insert(
+        "spdx".to_string(),
+        GitRepository::new(
+            "https://github.com/spdx/license-list-data.git".to_string(),
+            "main".to_string(),
+            get_local_path("spdx-license-list-data")?,
+        ),
+    );
+    
+    Ok(repos)
+}
+
+/// Get local storage path for a repository
+fn get_local_path(repo_name: &str) -> Result<Utf8PathBuf> {
+    let home_dir = dirs::home_dir()
+        .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
+    
+    let local_path = Utf8PathBuf::from_path_buf(home_dir)
+        .map_err(|_| anyhow::anyhow!("Invalid UTF-8 in home directory path"))?
+        .join(".local")
+        .join("share")
+        .join("spandx")
+        .join(repo_name);
+    
+    Ok(local_path)
+}
+
+/// Update all repositories and rebuild cache indices
+pub async fn sync_repositories(repos: &mut HashMap<String, GitRepository>) -> Result<()> {
+    use tracing::{info, warn};
+    
+    info!("Syncing Git repositories...");
+    
+    // Update all repositories in parallel
+    let futures: Vec<_> = repos.values_mut().map(|repo| async move {
+        match repo.update().await {
+            Ok(_) => {
+                info!("Successfully updated repository: {}", repo.name());
+                Ok(())
+            }
+            Err(e) => {
+                warn!("Failed to update repository {}: {}", repo.name(), e);
+                Err(e)
+            }
+        }
+    }).collect();
+    
+    // Wait for all repositories to complete
+    let results = futures::future::join_all(futures).await;
+    
+    // Check if any updates failed
+    let mut errors = Vec::new();
+    for result in results {
+        if let Err(e) = result {
+            errors.push(e);
+        }
+    }
+    
+    if !errors.is_empty() {
+        warn!("Some repositories failed to update: {} errors", errors.len());
+        // Continue with cache rebuild even if some repositories failed
+    }
+    
+    info!("Repository sync completed");
+    Ok(())
+}
\ No newline at end of file
src/git/operations.rs
@@ -0,0 +1,348 @@
+use crate::cache::Cache;
+use crate::git::GitRepository;
+use anyhow::Result;
+use std::collections::HashMap;
+use tracing::{info, warn, debug};
+
+/// High-level Git operations for cache management
+pub struct GitOperations {
+    repositories: HashMap<String, GitRepository>,
+}
+
+impl GitOperations {
+    pub fn new(repositories: HashMap<String, GitRepository>) -> Self {
+        Self { repositories }
+    }
+    
+    /// Get a repository by name
+    pub fn get_repository(&self, name: &str) -> Option<&GitRepository> {
+        self.repositories.get(name)
+    }
+    
+    /// Get a mutable repository by name
+    pub fn get_repository_mut(&mut self, name: &str) -> Option<&mut GitRepository> {
+        self.repositories.get_mut(name)
+    }
+    
+    /// Update all repositories
+    pub async fn update_all(&mut self) -> Result<UpdateResult> {
+        info!("Updating all Git repositories");
+        
+        let mut successful = Vec::new();
+        let mut failed = Vec::new();
+        
+        for (name, repo) in &mut self.repositories {
+            match repo.update().await {
+                Ok(_) => {
+                    info!("Successfully updated repository: {}", name);
+                    successful.push(name.clone());
+                }
+                Err(e) => {
+                    warn!("Failed to update repository {}: {}", name, e);
+                    failed.push((name.clone(), e));
+                }
+            }
+        }
+        
+        Ok(UpdateResult {
+            successful,
+            failed,
+        })
+    }
+    
+    /// Update a specific repository
+    pub async fn update_repository(&mut self, name: &str) -> Result<()> {
+        if let Some(repo) = self.repositories.get_mut(name) {
+            repo.update().await?;
+            info!("Successfully updated repository: {}", name);
+            Ok(())
+        } else {
+            Err(anyhow::anyhow!("Repository not found: {}", name))
+        }
+    }
+    
+    /// Build cache indices from all repositories
+    pub async fn build_cache_indices(&self, cache: &mut Cache) -> Result<BuildResult> {
+        info!("Building cache indices from Git repositories");
+        
+        let mut built_indices = Vec::new();
+        let mut errors = Vec::new();
+        
+        // Process each repository that has cache data
+        for (name, repo) in &self.repositories {
+            if !repo.has_cache_data() {
+                debug!("Repository {} has no cache data, skipping", name);
+                continue;
+            }
+            
+            match self.build_cache_for_repository(cache, repo).await {
+                Ok(count) => {
+                    info!("Built cache index for {} with {} entries", name, count);
+                    built_indices.push((name.clone(), count));
+                }
+                Err(e) => {
+                    warn!("Failed to build cache for repository {}: {}", name, e);
+                    errors.push((name.clone(), e));
+                }
+            }
+        }
+        
+        Ok(BuildResult {
+            built_indices,
+            errors,
+        })
+    }
+    
+    /// Build cache for a specific repository
+    async fn build_cache_for_repository(&self, cache: &mut Cache, repo: &GitRepository) -> Result<usize> {
+        let cache_dir = repo.cache_index_dir();
+        
+        if !cache_dir.exists() {
+            return Ok(0);
+        }
+        
+        let mut total_entries = 0;
+        
+        // List all package manager directories
+        let mut entries = tokio::fs::read_dir(&cache_dir).await?;
+        while let Some(entry) = entries.next_entry().await? {
+            let path = entry.path();
+            if path.is_dir() {
+                if let Some(package_manager) = path.file_name().and_then(|n| n.to_str()) {
+                    // Skip hidden directories
+                    if package_manager.starts_with('.') {
+                        continue;
+                    }
+                    
+                    debug!("Building cache for package manager: {}", package_manager);
+                    match self.import_package_manager_data(cache, &cache_dir, package_manager).await {
+                        Ok(count) => {
+                            total_entries += count;
+                            debug!("Imported {} entries for {}", count, package_manager);
+                        }
+                        Err(e) => {
+                            warn!("Failed to import data for {}: {}", package_manager, e);
+                        }
+                    }
+                }
+            }
+        }
+        
+        Ok(total_entries)
+    }
+    
+    /// Import data for a specific package manager
+    async fn import_package_manager_data(&self, cache: &mut Cache, cache_dir: &camino::Utf8Path, package_manager: &str) -> Result<usize> {
+        let pm_dir = cache_dir.join(package_manager);
+        let mut total_entries = 0;
+        
+        // Process all bucket directories (00-ff)
+        let mut entries = tokio::fs::read_dir(&pm_dir).await?;
+        while let Some(entry) = entries.next_entry().await? {
+            let path = entry.path();
+            if path.is_dir() {
+                if let Some(bucket) = path.file_name().and_then(|n| n.to_str()) {
+                    // Validate bucket name (should be 2-digit hex)
+                    if bucket.len() == 2 && bucket.chars().all(|c| c.is_ascii_hexdigit()) {
+                        match self.import_bucket_data(cache, &pm_dir, bucket, package_manager).await {
+                            Ok(count) => {
+                                total_entries += count;
+                            }
+                            Err(e) => {
+                                debug!("Failed to import bucket {} for {}: {}", bucket, package_manager, e);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        
+        Ok(total_entries)
+    }
+    
+    /// Import data for a specific bucket
+    async fn import_bucket_data(&self, cache: &mut Cache, pm_dir: &camino::Utf8Path, bucket: &str, package_manager: &str) -> Result<usize> {
+        let bucket_dir = pm_dir.join(bucket);
+        let data_file = bucket_dir.join(package_manager);
+        
+        if !data_file.exists() {
+            return Ok(0);
+        }
+        
+        // Read the CSV data file
+        let content = tokio::fs::read_to_string(&data_file).await?;
+        let mut entry_count = 0;
+        
+        for line in content.lines() {
+            let line = line.trim();
+            if line.is_empty() {
+                continue;
+            }
+            
+            // Parse CSV line: "name","version","license1-|-license2"
+            match self.parse_cache_line(line) {
+                Ok((name, version, licenses)) => {
+                    cache.set_licenses(&name, &version, package_manager, licenses).await?;
+                    entry_count += 1;
+                }
+                Err(e) => {
+                    debug!("Failed to parse cache line: {} - {}", line, e);
+                }
+            }
+        }
+        
+        Ok(entry_count)
+    }
+    
+    /// Parse a cache line from CSV format
+    fn parse_cache_line(&self, line: &str) -> Result<(String, String, Vec<String>)> {
+        let mut reader = csv::ReaderBuilder::new()
+            .has_headers(false)
+            .from_reader(line.as_bytes());
+        
+        if let Some(result) = reader.records().next() {
+            let record = result?;
+            if record.len() >= 3 {
+                let name = record[0].to_string();
+                let version = record[1].to_string();
+                let licenses_str = &record[2];
+                
+                let licenses = if licenses_str.is_empty() {
+                    Vec::new()
+                } else {
+                    licenses_str.split("-|-").map(|s| s.to_string()).collect()
+                };
+                
+                return Ok((name, version, licenses));
+            }
+        }
+        
+        Err(anyhow::anyhow!("Invalid CSV line: {}", line))
+    }
+    
+    /// Get status of all repositories
+    pub async fn get_all_status(&self) -> HashMap<String, RepositoryStatusInfo> {
+        let mut statuses = HashMap::new();
+        
+        for (name, repo) in &self.repositories {
+            let status = match repo.status().await {
+                Ok(status) => status,
+                Err(e) => {
+                    warn!("Failed to get status for repository {}: {}", name, e);
+                    continue;
+                }
+            };
+            
+            let last_commit = repo.last_commit_hash().await.unwrap_or_else(|_| "unknown".to_string());
+            let has_cache = repo.has_cache_data();
+            
+            statuses.insert(name.clone(), RepositoryStatusInfo {
+                status,
+                last_commit,
+                has_cache_data: has_cache,
+                local_path: repo.local_path().to_path_buf(),
+            });
+        }
+        
+        statuses
+    }
+    
+    /// Read a file from a specific repository
+    pub async fn read_file(&self, repo_name: &str, file_path: &str) -> Result<String> {
+        if let Some(repo) = self.repositories.get(repo_name) {
+            repo.read_file(file_path).await
+        } else {
+            Err(anyhow::anyhow!("Repository not found: {}", repo_name))
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct UpdateResult {
+    pub successful: Vec<String>,
+    pub failed: Vec<(String, anyhow::Error)>,
+}
+
+impl UpdateResult {
+    pub fn is_success(&self) -> bool {
+        self.failed.is_empty()
+    }
+    
+    pub fn partial_success(&self) -> bool {
+        !self.successful.is_empty() && !self.failed.is_empty()
+    }
+}
+
+#[derive(Debug)]
+pub struct BuildResult {
+    pub built_indices: Vec<(String, usize)>,
+    pub errors: Vec<(String, anyhow::Error)>,
+}
+
+impl BuildResult {
+    pub fn total_entries(&self) -> usize {
+        self.built_indices.iter().map(|(_, count)| count).sum()
+    }
+    
+    pub fn is_success(&self) -> bool {
+        self.errors.is_empty()
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct RepositoryStatusInfo {
+    pub status: crate::git::repository::RepositoryStatus,
+    pub last_commit: String,
+    pub has_cache_data: bool,
+    pub local_path: camino::Utf8PathBuf,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use tempfile::TempDir;
+    use camino::Utf8PathBuf;
+    
+    #[test]
+    fn test_parse_cache_line() {
+        let ops = GitOperations::new(HashMap::new());
+        
+        // Test normal case
+        let result = ops.parse_cache_line("\"rails\",\"7.0.0\",\"MIT-|-Apache-2.0\"").unwrap();
+        assert_eq!(result.0, "rails");
+        assert_eq!(result.1, "7.0.0");
+        assert_eq!(result.2, vec!["MIT", "Apache-2.0"]);
+        
+        // Test empty licenses
+        let result = ops.parse_cache_line("\"unknown\",\"1.0.0\",\"\"").unwrap();
+        assert_eq!(result.0, "unknown");
+        assert_eq!(result.1, "1.0.0");
+        assert!(result.2.is_empty());
+        
+        // Test single license
+        let result = ops.parse_cache_line("\"sinatra\",\"2.0.0\",\"MIT\"").unwrap();
+        assert_eq!(result.0, "sinatra");
+        assert_eq!(result.1, "2.0.0");
+        assert_eq!(result.2, vec!["MIT"]);
+    }
+    
+    #[tokio::test]
+    async fn test_git_operations_creation() {
+        let temp_dir = TempDir::new().unwrap();
+        let path = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).unwrap();
+        
+        let mut repos = HashMap::new();
+        repos.insert(
+            "test".to_string(),
+            GitRepository::new(
+                "https://github.com/example/repo.git".to_string(),
+                "main".to_string(),
+                path.join("test-repo"),
+            ),
+        );
+        
+        let ops = GitOperations::new(repos);
+        assert!(ops.get_repository("test").is_some());
+        assert!(ops.get_repository("nonexistent").is_none());
+    }
+}
\ No newline at end of file
src/git/repository.rs
@@ -0,0 +1,314 @@
+use anyhow::Result;
+use camino::{Utf8Path, Utf8PathBuf};
+use git2::{BranchType, Repository, RemoteCallbacks, FetchOptions};
+use std::path::Path;
+use tracing::{debug, info};
+
+/// Represents a Git repository for cache management
+#[derive(Debug, Clone)]
+pub struct GitRepository {
+    url: String,
+    branch: String,
+    local_path: Utf8PathBuf,
+    name: String,
+}
+
+impl GitRepository {
+    pub fn new(url: String, branch: String, local_path: Utf8PathBuf) -> Self {
+        let name = local_path
+            .file_name()
+            .unwrap_or("unknown")
+            .to_string();
+        
+        Self {
+            url,
+            branch,
+            local_path,
+            name,
+        }
+    }
+    
+    pub fn name(&self) -> &str {
+        &self.name
+    }
+    
+    pub fn url(&self) -> &str {
+        &self.url
+    }
+    
+    pub fn branch(&self) -> &str {
+        &self.branch
+    }
+    
+    pub fn local_path(&self) -> &Utf8Path {
+        &self.local_path
+    }
+    
+    /// Check if the repository exists locally
+    pub fn exists(&self) -> bool {
+        self.local_path.join(".git").exists()
+    }
+    
+    /// Update the repository (clone if not exists, pull if exists)
+    pub async fn update(&mut self) -> Result<()> {
+        if self.exists() {
+            self.pull().await
+        } else {
+            self.clone().await
+        }
+    }
+    
+    /// Clone the repository
+    pub async fn clone(&self) -> Result<()> {
+        info!("Cloning repository {} to {:?}", self.url, self.local_path);
+        
+        // Ensure parent directory exists
+        if let Some(parent) = self.local_path.parent() {
+            tokio::fs::create_dir_all(parent).await?;
+        }
+        
+        // Perform clone operation in blocking thread
+        let url = self.url.clone();
+        let branch = self.branch.clone();
+        let local_path = self.local_path.clone();
+        
+        tokio::task::spawn_blocking(move || -> Result<()> {
+            let mut builder = git2::build::RepoBuilder::new();
+            
+            // Configure for shallow clone
+            let mut fetch_opts = FetchOptions::new();
+            fetch_opts.depth(1);
+            
+            // Set up progress callback
+            let mut callbacks = RemoteCallbacks::new();
+            callbacks.pack_progress(|_stage, current, total| {
+                if current > 0 {
+                    debug!(
+                        "Clone progress: {}/{}",
+                        current,
+                        total
+                    );
+                }
+            });
+            
+            fetch_opts.remote_callbacks(callbacks);
+            builder.fetch_options(fetch_opts);
+            
+            // Set branch
+            builder.branch(&branch);
+            
+            // Perform clone
+            let repo = builder.clone(&url, Path::new(&local_path))?;
+            
+            debug!("Successfully cloned repository to {:?}", local_path);
+            
+            // Verify checkout
+            let head = repo.head()?;
+            if let Some(name) = head.shorthand() {
+                debug!("Checked out branch: {}", name);
+            }
+            
+            Ok(())
+        }).await??;
+        
+        info!("Clone completed for {}", self.name);
+        Ok(())
+    }
+    
+    /// Pull latest changes
+    pub async fn pull(&self) -> Result<()> {
+        info!("Pulling latest changes for repository {}", self.name);
+        
+        let local_path = self.local_path.clone();
+        let branch = self.branch.clone();
+        
+        tokio::task::spawn_blocking(move || -> Result<()> {
+            let repo = Repository::open(Path::new(&local_path))?;
+            
+            // Fetch from origin
+            let mut remote = repo.find_remote("origin")?;
+            
+            let mut fetch_opts = FetchOptions::new();
+            fetch_opts.depth(1);
+            
+            // Set up progress callback
+            let mut callbacks = RemoteCallbacks::new();
+            callbacks.pack_progress(|_stage, current, total| {
+                if current > 0 {
+                    debug!(
+                        "Fetch progress: {}/{}",
+                        current,
+                        total
+                    );
+                }
+            });
+            
+            fetch_opts.remote_callbacks(callbacks);
+            
+            // Fetch the branch
+            let refspec = format!("refs/heads/{}:refs/remotes/origin/{}", branch, branch);
+            remote.fetch(&[&refspec], Some(&mut fetch_opts), None)?;
+            
+            debug!("Fetch completed");
+            
+            // Find the target branch
+            let remote_branch_name = format!("origin/{}", branch);
+            let remote_branch = repo.find_branch(&remote_branch_name, BranchType::Remote)?;
+            let remote_commit = remote_branch.get().peel_to_commit()?;
+            
+            // Checkout the commit
+            let tree = remote_commit.tree()?;
+            repo.checkout_tree(tree.as_object(), None)?;
+            
+            // Update HEAD to point to the new commit
+            repo.set_head_detached(remote_commit.id())?;
+            
+            debug!("Checked out latest commit: {}", remote_commit.id());
+            
+            Ok(())
+        }).await??;
+        
+        info!("Pull completed for {}", self.name);
+        Ok(())
+    }
+    
+    /// Read a file from the repository
+    pub async fn read_file<P: AsRef<Utf8Path>>(&self, path: P) -> Result<String> {
+        let file_path = self.local_path.join(path.as_ref());
+        
+        if !file_path.exists() {
+            return Err(anyhow::anyhow!(
+                "File does not exist: {:?}",
+                file_path
+            ));
+        }
+        
+        let content = tokio::fs::read_to_string(&file_path).await?;
+        Ok(content)
+    }
+    
+    /// List files in a directory within the repository
+    pub async fn list_files<P: AsRef<Utf8Path>>(&self, dir_path: P) -> Result<Vec<Utf8PathBuf>> {
+        let full_path = self.local_path.join(dir_path.as_ref());
+        
+        if !full_path.exists() {
+            return Ok(Vec::new());
+        }
+        
+        let mut files = Vec::new();
+        let mut entries = tokio::fs::read_dir(&full_path).await?;
+        
+        while let Some(entry) = entries.next_entry().await? {
+            let path = entry.path();
+            if path.is_file() {
+                if let Ok(utf8_path) = Utf8PathBuf::from_path_buf(path) {
+                    // Make path relative to repository root
+                    if let Ok(relative_path) = utf8_path.strip_prefix(&self.local_path) {
+                        files.push(relative_path.to_path_buf());
+                    }
+                }
+            }
+        }
+        
+        Ok(files)
+    }
+    
+    /// Get the cache index directory for this repository
+    pub fn cache_index_dir(&self) -> Utf8PathBuf {
+        self.local_path.join(".index")
+    }
+    
+    /// Check if the repository has cache data
+    pub fn has_cache_data(&self) -> bool {
+        self.cache_index_dir().exists()
+    }
+    
+    /// Get the last commit hash
+    pub async fn last_commit_hash(&self) -> Result<String> {
+        let local_path = self.local_path.clone();
+        
+        tokio::task::spawn_blocking(move || -> Result<String> {
+            let repo = Repository::open(Path::new(&local_path))?;
+            let head = repo.head()?;
+            let commit = head.peel_to_commit()?;
+            Ok(commit.id().to_string())
+        }).await?
+    }
+    
+    /// Get repository status
+    pub async fn status(&self) -> Result<RepositoryStatus> {
+        if !self.exists() {
+            return Ok(RepositoryStatus::NotCloned);
+        }
+        
+        let local_path = self.local_path.clone();
+        
+        tokio::task::spawn_blocking(move || -> Result<RepositoryStatus> {
+            let repo = Repository::open(Path::new(&local_path))?;
+            
+            // Check if there are any uncommitted changes
+            let statuses = repo.statuses(None)?;
+            if !statuses.is_empty() {
+                return Ok(RepositoryStatus::Dirty);
+            }
+            
+            // Check if we're ahead/behind remote
+            let head = repo.head()?;
+            let local_commit = head.peel_to_commit()?;
+            
+            Ok(RepositoryStatus::Clean {
+                commit_hash: local_commit.id().to_string(),
+                commit_message: local_commit.message().unwrap_or("").to_string(),
+            })
+        }).await?
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum RepositoryStatus {
+    NotCloned,
+    Dirty,
+    Clean {
+        commit_hash: String,
+        commit_message: String,
+    },
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use tempfile::TempDir;
+    
+    #[test]
+    fn test_repository_creation() {
+        let temp_dir = TempDir::new().unwrap();
+        let path = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).unwrap();
+        
+        let repo = GitRepository::new(
+            "https://github.com/example/repo.git".to_string(),
+            "main".to_string(),
+            path.join("test-repo"),
+        );
+        
+        assert_eq!(repo.url(), "https://github.com/example/repo.git");
+        assert_eq!(repo.branch(), "main");
+        assert_eq!(repo.name(), "test-repo");
+        assert!(!repo.exists());
+    }
+    
+    #[test]
+    fn test_cache_paths() {
+        let temp_dir = TempDir::new().unwrap();
+        let path = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).unwrap();
+        
+        let repo = GitRepository::new(
+            "https://github.com/example/repo.git".to_string(),
+            "main".to_string(),
+            path.join("test-repo"),
+        );
+        
+        let cache_dir = repo.cache_index_dir();
+        assert_eq!(cache_dir, path.join("test-repo").join(".index"));
+        assert!(!repo.has_cache_data());
+    }
+}
\ No newline at end of file
src/parsers/dotnet/csproj.rs
@@ -0,0 +1,176 @@
+use crate::core::{
+    parser::{Parser, ParserError, ParserResult},
+    Dependency, DependencyCollection,
+};
+use async_trait::async_trait;
+use camino::Utf8Path;
+use roxmltree::{Document, Node};
+use std::collections::HashMap;
+use tracing::debug;
+
+#[derive(Debug)]
+pub struct CsprojParser;
+
+impl CsprojParser {
+    pub fn new() -> Self {
+        Self
+    }
+    
+    fn matches_filename(&self, filename: &str) -> bool {
+        filename.ends_with(".csproj") || filename.ends_with(".props")
+    }
+}
+
+impl Default for CsprojParser {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[async_trait]
+impl Parser for CsprojParser {
+    fn can_parse(&self, path: &Utf8Path) -> bool {
+        path.file_name()
+            .map(|name| self.matches_filename(name))
+            .unwrap_or(false)
+    }
+    
+    fn name(&self) -> &'static str {
+        "csproj"
+    }
+    
+    fn file_patterns(&self) -> Vec<&'static str> {
+        vec!["*.csproj", "*.props"]
+    }
+
+    async fn parse(&self, path: &Utf8Path) -> ParserResult<DependencyCollection> {
+        debug!("Parsing .csproj/.props at: {}", path);
+        
+        let content = tokio::fs::read_to_string(path)
+            .await
+            .map_err(|_| ParserError::FileNotFound(path.to_path_buf()))?;
+        
+        let document = Document::parse(&content)
+            .map_err(|e| ParserError::XmlError(e.to_string()))?;
+        
+        let mut dependencies = DependencyCollection::new();
+        
+        let root = document.root_element();
+        
+        // Find all PackageReference and GlobalPackageReference nodes
+        self.parse_package_references(&root, path, &mut dependencies)?;
+        
+        debug!("Found {} dependencies in .csproj/.props", dependencies.len());
+        Ok(dependencies)
+    }
+}
+
+impl CsprojParser {
+    fn parse_package_references(
+        &self,
+        node: &Node,
+        path: &Utf8Path,
+        dependencies: &mut DependencyCollection,
+    ) -> ParserResult<()> {
+        // Recursively search for PackageReference and GlobalPackageReference
+        if node.has_tag_name("PackageReference") || node.has_tag_name("GlobalPackageReference") {
+            if let Some(dependency) = self.create_dependency_from_package_reference(path, node)? {
+                dependencies.add(dependency);
+            }
+        }
+        
+        // Continue searching child nodes
+        for child in node.children() {
+            self.parse_package_references(&child, path, dependencies)?;
+        }
+        
+        Ok(())
+    }
+    
+    fn create_dependency_from_package_reference(
+        &self,
+        path: &Utf8Path,
+        node: &Node,
+    ) -> ParserResult<Option<Dependency>> {
+        // Extract package name from Include or Update attribute
+        let package_name = node.attribute("Include")
+            .or_else(|| node.attribute("Update"))
+            .unwrap_or("")
+            .to_string();
+        
+        if package_name.is_empty() {
+            return Ok(None);
+        }
+        
+        // Extract version from Version attribute or child element
+        let mut version = node.attribute("Version")
+            .unwrap_or("")
+            .to_string();
+        
+        // If no version attribute, look for Version child element
+        if version.is_empty() {
+            for child in node.children() {
+                if child.has_tag_name("Version") {
+                    if let Some(text) = child.text() {
+                        version = text.trim().to_string();
+                        break;
+                    }
+                }
+            }
+        }
+        
+        if version.is_empty() {
+            return Ok(None);
+        }
+        
+        let mut meta = HashMap::new();
+        
+        // Extract additional metadata from attributes
+        if let Some(private_assets) = node.attribute("PrivateAssets") {
+            meta.insert("private_assets".to_string(), private_assets.to_string());
+        }
+        
+        if let Some(include_assets) = node.attribute("IncludeAssets") {
+            meta.insert("include_assets".to_string(), include_assets.to_string());
+        }
+        
+        if let Some(exclude_assets) = node.attribute("ExcludeAssets") {
+            meta.insert("exclude_assets".to_string(), exclude_assets.to_string());
+        }
+        
+        // Extract metadata from child elements
+        for child in node.children() {
+            if let Some(text) = child.text() {
+                let text = text.trim();
+                if !text.is_empty() {
+                    match child.tag_name().name() {
+                        "PrivateAssets" => {
+                            meta.insert("private_assets".to_string(), text.to_string());
+                        }
+                        "IncludeAssets" => {
+                            meta.insert("include_assets".to_string(), text.to_string());
+                        }
+                        "ExcludeAssets" => {
+                            meta.insert("exclude_assets".to_string(), text.to_string());
+                        }
+                        "Condition" => {
+                            meta.insert("condition".to_string(), text.to_string());
+                        }
+                        _ => {}
+                    }
+                }
+            }
+        }
+        
+        // Check for Condition attribute
+        if let Some(condition) = node.attribute("Condition") {
+            meta.insert("condition".to_string(), condition.to_string());
+        }
+        
+        let mut dependency = Dependency::new(package_name, version);
+        dependency.location = path.to_path_buf();
+        dependency.metadata = meta;
+        
+        Ok(Some(dependency))
+    }
+}
\ No newline at end of file
src/parsers/dotnet/mod.rs
@@ -0,0 +1,8 @@
+pub mod csproj;
+pub mod packages_config;
+
+#[cfg(test)]
+mod tests;
+
+pub use csproj::CsprojParser;
+pub use packages_config::PackagesConfigParser;
\ No newline at end of file
src/parsers/dotnet/packages_config.rs
@@ -0,0 +1,162 @@
+use crate::core::{
+    parser::{Parser, ParserError, ParserResult},
+    Dependency, DependencyCollection,
+};
+use async_trait::async_trait;
+use camino::Utf8Path;
+use roxmltree::{Document, Node};
+use std::collections::HashMap;
+use tracing::debug;
+
+#[derive(Debug)]
+pub struct PackagesConfigParser;
+
+impl PackagesConfigParser {
+    pub fn new() -> Self {
+        Self
+    }
+    
+    fn matches_filename(&self, filename: &str) -> bool {
+        filename == "packages.config"
+    }
+}
+
+impl Default for PackagesConfigParser {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[async_trait]
+impl Parser for PackagesConfigParser {
+    fn can_parse(&self, path: &Utf8Path) -> bool {
+        path.file_name()
+            .map(|name| self.matches_filename(name))
+            .unwrap_or(false)
+    }
+    
+    fn name(&self) -> &'static str {
+        "packages_config"
+    }
+    
+    fn file_patterns(&self) -> Vec<&'static str> {
+        vec!["packages.config"]
+    }
+
+    async fn parse(&self, path: &Utf8Path) -> ParserResult<DependencyCollection> {
+        debug!("Parsing packages.config at: {}", path);
+        
+        let content = tokio::fs::read_to_string(path)
+            .await
+            .map_err(|_| ParserError::FileNotFound(path.to_path_buf()))?;
+        
+        let document = Document::parse(&content)
+            .map_err(|e| ParserError::XmlError(e.to_string()))?;
+        
+        let mut dependencies = DependencyCollection::new();
+        
+        let root = document.root_element();
+        
+        // Find all package nodes
+        self.parse_packages(&root, path, &mut dependencies)?;
+        
+        debug!("Found {} dependencies in packages.config", dependencies.len());
+        Ok(dependencies)
+    }
+}
+
+impl PackagesConfigParser {
+    fn parse_packages(
+        &self,
+        node: &Node,
+        path: &Utf8Path,
+        dependencies: &mut DependencyCollection,
+    ) -> ParserResult<()> {
+        // Look for package elements
+        if node.has_tag_name("package") {
+            if let Some(dependency) = self.create_dependency_from_package(path, node)? {
+                dependencies.add(dependency);
+            }
+        }
+        
+        // Continue searching child nodes
+        for child in node.children() {
+            self.parse_packages(&child, path, dependencies)?;
+        }
+        
+        Ok(())
+    }
+    
+    fn create_dependency_from_package(
+        &self,
+        path: &Utf8Path,
+        node: &Node,
+    ) -> ParserResult<Option<Dependency>> {
+        // Extract package id and version from attributes
+        let package_id = node.attribute("id")
+            .unwrap_or("")
+            .to_string();
+        
+        let mut version = node.attribute("version")
+            .unwrap_or("")
+            .to_string();
+        
+        // If no version attribute, look for version child element
+        if version.is_empty() {
+            for child in node.children() {
+                if child.has_tag_name("version") {
+                    if let Some(text) = child.text() {
+                        version = text.trim().to_string();
+                        break;
+                    }
+                }
+            }
+        }
+        
+        if package_id.is_empty() || version.is_empty() {
+            return Ok(None);
+        }
+        
+        let mut meta = HashMap::new();
+        
+        // Extract additional metadata from attributes
+        if let Some(target_framework) = node.attribute("targetFramework") {
+            meta.insert("target_framework".to_string(), target_framework.to_string());
+        }
+        
+        if let Some(development_dependency) = node.attribute("developmentDependency") {
+            meta.insert("development_dependency".to_string(), development_dependency.to_string());
+        }
+        
+        if let Some(require_reinstallation) = node.attribute("requireReinstallation") {
+            meta.insert("require_reinstallation".to_string(), require_reinstallation.to_string());
+        }
+        
+        // Extract metadata from child elements
+        for child in node.children() {
+            if let Some(text) = child.text() {
+                let text = text.trim();
+                if !text.is_empty() {
+                    match child.tag_name().name() {
+                        "targetFramework" => {
+                            meta.insert("target_framework".to_string(), text.to_string());
+                        }
+                        "developmentDependency" => {
+                            meta.insert("development_dependency".to_string(), text.to_string());
+                        }
+                        "requireReinstallation" => {
+                            meta.insert("require_reinstallation".to_string(), text.to_string());
+                        }
+                        _ => {}
+                    }
+                }
+            }
+        }
+        
+        let mut dependency = Dependency::new(package_id, version);
+        dependency.location = path.to_path_buf();
+        dependency.metadata = meta;
+        
+        Ok(Some(dependency))
+    }
+}
\ No newline at end of file
src/parsers/dotnet/tests.rs
@@ -0,0 +1,276 @@
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::core::parser::Parser;
+    use crate::parsers::{CsprojParser, PackagesConfigParser};
+    use camino::Utf8PathBuf;
+    use std::fs;
+    use tempfile::tempdir;
+
+    #[test]
+    fn test_csproj_parser_can_parse() {
+        let parser = CsprojParser::new();
+        
+        assert!(parser.can_parse(Utf8PathBuf::from("/path/to/project.csproj").as_path()));
+        assert!(parser.can_parse(Utf8PathBuf::from("/path/to/Directory.Build.props").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/packages.config").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/project.sln").as_path()));
+    }
+
+    #[test]
+    fn test_packages_config_parser_can_parse() {
+        let parser = PackagesConfigParser::new();
+        
+        assert!(parser.can_parse(Utf8PathBuf::from("/path/to/packages.config").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/project.csproj").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/project.sln").as_path()));
+    }
+
+    #[tokio::test]
+    async fn test_csproj_parser_parse_basic() {
+        let content = r#"<?xml version="1.0" encoding="utf-8"?>
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+  </PropertyGroup>
+  
+  <ItemGroup>
+    <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
+    <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
+    <PackageReference Include="System.Text.Json" Version="6.0.0" PrivateAssets="all" />
+  </ItemGroup>
+</Project>"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("test.csproj");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = CsprojParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 3);
+        
+        // Check Newtonsoft.Json package
+        let newtonsoft = result.iter()
+            .find(|dep| dep.name == "Newtonsoft.Json")
+            .expect("Newtonsoft.Json package not found");
+        assert_eq!(newtonsoft.version, "13.0.1");
+        
+        // Check Microsoft.Extensions.Logging package
+        let logging = result.iter()
+            .find(|dep| dep.name == "Microsoft.Extensions.Logging")
+            .expect("Microsoft.Extensions.Logging package not found");
+        assert_eq!(logging.version, "6.0.0");
+        
+        // Check System.Text.Json package with PrivateAssets
+        let text_json = result.iter()
+            .find(|dep| dep.name == "System.Text.Json")
+            .expect("System.Text.Json package not found");
+        assert_eq!(text_json.version, "6.0.0");
+        assert_eq!(text_json.metadata.get("private_assets"), Some(&"all".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_csproj_parser_with_child_elements() {
+        let content = r#"<?xml version="1.0" encoding="utf-8"?>
+<Project Sdk="Microsoft.NET.Sdk">
+  <ItemGroup>
+    <PackageReference Include="EntityFramework">
+      <Version>6.4.4</Version>
+      <PrivateAssets>none</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+    </PackageReference>
+    <PackageReference Include="TestPackage" Update="true">
+      <Version>1.0.0</Version>
+      <ExcludeAssets>build</ExcludeAssets>
+    </PackageReference>
+  </ItemGroup>
+</Project>"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("test.csproj");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = CsprojParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 2);
+        
+        // Check EntityFramework package
+        let ef = result.iter()
+            .find(|dep| dep.name == "EntityFramework")
+            .expect("EntityFramework package not found");
+        assert_eq!(ef.version, "6.4.4");
+        assert_eq!(ef.metadata.get("private_assets"), Some(&"none".to_string()));
+        assert_eq!(ef.metadata.get("include_assets"), Some(&"runtime; build; native; contentfiles; analyzers".to_string()));
+        
+        // Check TestPackage with Update attribute
+        let test_pkg = result.iter()
+            .find(|dep| dep.name == "TestPackage")
+            .expect("TestPackage not found");
+        assert_eq!(test_pkg.version, "1.0.0");
+        assert_eq!(test_pkg.metadata.get("exclude_assets"), Some(&"build".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_packages_config_parser_parse_basic() {
+        let content = r#"<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="Newtonsoft.Json" version="13.0.1" targetFramework="net472" />
+  <package id="NUnit" version="3.13.2" targetFramework="net472" developmentDependency="true" />
+  <package id="EntityFramework" version="6.4.4" targetFramework="net472" />
+</packages>"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("packages.config");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = PackagesConfigParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 3);
+        
+        // Check Newtonsoft.Json package
+        let newtonsoft = result.iter()
+            .find(|dep| dep.name == "Newtonsoft.Json")
+            .expect("Newtonsoft.Json package not found");
+        assert_eq!(newtonsoft.version, "13.0.1");
+        assert_eq!(newtonsoft.metadata.get("target_framework"), Some(&"net472".to_string()));
+        
+        // Check NUnit package with developmentDependency
+        let nunit = result.iter()
+            .find(|dep| dep.name == "NUnit")
+            .expect("NUnit package not found");
+        assert_eq!(nunit.version, "3.13.2");
+        assert_eq!(nunit.metadata.get("development_dependency"), Some(&"true".to_string()));
+        
+        // Check EntityFramework package
+        let ef = result.iter()
+            .find(|dep| dep.name == "EntityFramework")
+            .expect("EntityFramework package not found");
+        assert_eq!(ef.version, "6.4.4");
+    }
+
+    #[tokio::test]
+    async fn test_packages_config_parser_with_child_elements() {
+        let content = r#"<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="TestPackage">
+    <version>1.0.0</version>
+    <targetFramework>net48</targetFramework>
+    <developmentDependency>false</developmentDependency>
+  </package>
+</packages>"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("packages.config");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = PackagesConfigParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 1);
+        
+        let package = &result.iter().next().unwrap();
+        assert_eq!(package.name, "TestPackage");
+        assert_eq!(package.version, "1.0.0");
+        assert_eq!(package.metadata.get("target_framework"), Some(&"net48".to_string()));
+        assert_eq!(package.metadata.get("development_dependency"), Some(&"false".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_csproj_parser_empty_project() {
+        let content = r#"<?xml version="1.0" encoding="utf-8"?>
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+  </PropertyGroup>
+</Project>"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("test.csproj");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = CsprojParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 0);
+    }
+
+    #[tokio::test]
+    async fn test_packages_config_parser_empty_packages() {
+        let content = r#"<?xml version="1.0" encoding="utf-8"?>
+<packages>
+</packages>"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("packages.config");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = PackagesConfigParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 0);
+    }
+
+    #[tokio::test]
+    async fn test_csproj_parser_missing_version() {
+        let content = r#"<?xml version="1.0" encoding="utf-8"?>
+<Project Sdk="Microsoft.NET.Sdk">
+  <ItemGroup>
+    <PackageReference Include="ValidPackage" Version="1.0.0" />
+    <PackageReference Include="NoVersionPackage" />
+    <PackageReference Include="EmptyVersionPackage" Version="" />
+  </ItemGroup>
+</Project>"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("test.csproj");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = CsprojParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        // Should only include ValidPackage
+        assert_eq!(result.len(), 1);
+        
+        let valid = result.iter()
+            .find(|dep| dep.name == "ValidPackage")
+            .expect("ValidPackage not found");
+        assert_eq!(valid.version, "1.0.0");
+    }
+
+    #[tokio::test]
+    async fn test_packages_config_parser_missing_required_fields() {
+        let content = r#"<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="ValidPackage" version="1.0.0" />
+  <package version="2.0.0" />
+  <package id="NoVersionPackage" />
+  <package id="" version="3.0.0" />
+</packages>"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("packages.config");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = PackagesConfigParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        // Should only include ValidPackage
+        assert_eq!(result.len(), 1);
+        
+        let valid = result.iter()
+            .find(|dep| dep.name == "ValidPackage")
+            .expect("ValidPackage not found");
+        assert_eq!(valid.version, "1.0.0");
+    }
+}
\ No newline at end of file
src/parsers/java/maven.rs
@@ -0,0 +1,169 @@
+use crate::core::{
+    parser::{Parser, ParserError, ParserResult},
+    Dependency, DependencyCollection,
+};
+use async_trait::async_trait;
+use camino::Utf8Path;
+use roxmltree::{Document, Node};
+use std::collections::HashMap;
+use tracing::debug;
+
+#[derive(Debug)]
+pub struct MavenParser;
+
+impl MavenParser {
+    pub fn new() -> Self {
+        Self
+    }
+    
+    fn matches_filename(&self, filename: &str) -> bool {
+        filename == "pom.xml"
+    }
+}
+
+impl Default for MavenParser {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[async_trait]
+impl Parser for MavenParser {
+    fn can_parse(&self, path: &Utf8Path) -> bool {
+        path.file_name()
+            .map(|name| self.matches_filename(name))
+            .unwrap_or(false)
+    }
+    
+    fn name(&self) -> &'static str {
+        "maven"
+    }
+    
+    fn file_patterns(&self) -> Vec<&'static str> {
+        vec!["pom.xml"]
+    }
+
+    async fn parse(&self, path: &Utf8Path) -> ParserResult<DependencyCollection> {
+        debug!("Parsing pom.xml at: {}", path);
+        
+        let content = tokio::fs::read_to_string(path)
+            .await
+            .map_err(|_| ParserError::FileNotFound(path.to_path_buf()))?;
+        
+        let document = Document::parse(&content)
+            .map_err(|e| ParserError::XmlError(e.to_string()))?;
+        
+        let mut dependencies = DependencyCollection::new();
+        
+        // Find all dependency nodes in the project
+        let root = document.root_element();
+        if let Some(dependencies_node) = self.find_dependencies_node(&root) {
+            for dependency_node in dependencies_node.children().filter(|n| n.has_tag_name("dependency")) {
+                if let Some(dependency) = self.create_dependency(path, &dependency_node)? {
+                    dependencies.add(dependency);
+                }
+            }
+        }
+        
+        debug!("Found {} dependencies in pom.xml", dependencies.len());
+        Ok(dependencies)
+    }
+}
+
+impl MavenParser {
+    fn find_dependencies_node<'a>(&self, root: &'a Node) -> Option<Node<'a, 'a>> {
+        // Look for project/dependencies
+        for child in root.children() {
+            if child.has_tag_name("project") {
+                for project_child in child.children() {
+                    if project_child.has_tag_name("dependencies") {
+                        return Some(project_child);
+                    }
+                }
+            }
+            // Also check if root is already project
+            if child.has_tag_name("dependencies") {
+                return Some(child);
+            }
+        }
+        
+        // If root is project, check direct children
+        if root.has_tag_name("project") {
+            for child in root.children() {
+                if child.has_tag_name("dependencies") {
+                    return Some(child);
+                }
+            }
+        }
+        
+        None
+    }
+    
+    fn create_dependency(
+        &self,
+        path: &Utf8Path,
+        dependency_node: &Node,
+    ) -> ParserResult<Option<Dependency>> {
+        let mut group_id = String::new();
+        let mut artifact_id = String::new();
+        let mut version = String::new();
+        let mut scope = String::new();
+        let mut optional = String::new();
+        let mut dependency_type = String::new();
+        let mut classifier = String::new();
+        
+        // Extract dependency information from child nodes
+        for child in dependency_node.children() {
+            if let Some(text) = child.text() {
+                match child.tag_name().name() {
+                    "groupId" => group_id = text.trim().to_string(),
+                    "artifactId" => artifact_id = text.trim().to_string(),
+                    "version" => version = text.trim().to_string(),
+                    "scope" => scope = text.trim().to_string(),
+                    "optional" => optional = text.trim().to_string(),
+                    "type" => dependency_type = text.trim().to_string(),
+                    "classifier" => classifier = text.trim().to_string(),
+                    _ => {}
+                }
+            }
+        }
+        
+        // Skip dependencies with Maven variables that we can't resolve
+        if group_id.contains("${") || artifact_id.contains("${") || version.contains("${") {
+            debug!("Skipping dependency with unresolved variables: {}:{}:{}", group_id, artifact_id, version);
+            return Ok(None);
+        }
+        
+        if group_id.is_empty() || artifact_id.is_empty() || version.is_empty() {
+            return Ok(None);
+        }
+        
+        let name = format!("{}:{}", group_id, artifact_id);
+        
+        let mut meta = HashMap::new();
+        meta.insert("group_id".to_string(), group_id);
+        meta.insert("artifact_id".to_string(), artifact_id);
+        
+        if !scope.is_empty() {
+            meta.insert("scope".to_string(), scope);
+        }
+        
+        if !optional.is_empty() {
+            meta.insert("optional".to_string(), optional);
+        }
+        
+        if !dependency_type.is_empty() {
+            meta.insert("type".to_string(), dependency_type);
+        }
+        
+        if !classifier.is_empty() {
+            meta.insert("classifier".to_string(), classifier);
+        }
+        
+        let mut dependency = Dependency::new(name, version);
+        dependency.location = path.to_path_buf();
+        dependency.metadata = meta;
+        
+        Ok(Some(dependency))
+    }
+}
\ No newline at end of file
src/parsers/java/mod.rs
@@ -0,0 +1,6 @@
+pub mod maven;
+
+#[cfg(test)]
+mod tests;
+
+pub use maven::MavenParser;
\ No newline at end of file
src/parsers/java/tests.rs
@@ -0,0 +1,258 @@
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::core::parser::Parser;
+    use crate::parsers::MavenParser;
+    use camino::Utf8PathBuf;
+    use std::fs;
+    use tempfile::tempdir;
+
+    #[test]
+    fn test_maven_parser_can_parse() {
+        let parser = MavenParser::new();
+        
+        assert!(parser.can_parse(Utf8PathBuf::from("/path/to/pom.xml").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/build.gradle").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/package.json").as_path()));
+    }
+
+    #[tokio::test]
+    async fn test_maven_parser_parse_basic() {
+        let content = r#"<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    
+    <groupId>com.example</groupId>
+    <artifactId>test-project</artifactId>
+    <version>1.0.0</version>
+    
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>3.8.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-core</artifactId>
+            <version>5.3.0</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>3.6.0</version>
+            <scope>test</scope>
+            <optional>true</optional>
+        </dependency>
+    </dependencies>
+</project>"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("pom.xml");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = MavenParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 3);
+        
+        // Check junit dependency
+        let junit = result.iter()
+            .find(|dep| dep.name == "junit:junit")
+            .expect("JUnit dependency not found");
+        assert_eq!(junit.version, "3.8.1");
+        assert_eq!(junit.metadata.get("group_id"), Some(&"junit".to_string()));
+        assert_eq!(junit.metadata.get("artifact_id"), Some(&"junit".to_string()));
+        
+        // Check spring dependency with scope
+        let spring = result.iter()
+            .find(|dep| dep.name == "org.springframework:spring-core")
+            .expect("Spring dependency not found");
+        assert_eq!(spring.version, "5.3.0");
+        assert_eq!(spring.metadata.get("scope"), Some(&"compile".to_string()));
+        
+        // Check mockito dependency with scope and optional
+        let mockito = result.iter()
+            .find(|dep| dep.name == "org.mockito:mockito-core")
+            .expect("Mockito dependency not found");
+        assert_eq!(mockito.version, "3.6.0");
+        assert_eq!(mockito.metadata.get("scope"), Some(&"test".to_string()));
+        assert_eq!(mockito.metadata.get("optional"), Some(&"true".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_maven_parser_empty_dependencies() {
+        let content = r#"<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.example</groupId>
+    <artifactId>test-project</artifactId>
+    <version>1.0.0</version>
+    
+    <dependencies>
+    </dependencies>
+</project>"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("pom.xml");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = MavenParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 0);
+    }
+
+    #[tokio::test]
+    async fn test_maven_parser_no_dependencies_section() {
+        let content = r#"<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.example</groupId>
+    <artifactId>test-project</artifactId>
+    <version>1.0.0</version>
+</project>"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("pom.xml");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = MavenParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 0);
+    }
+
+    #[tokio::test]
+    async fn test_maven_parser_with_variables() {
+        let content = r#"<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.example</groupId>
+    <artifactId>test-project</artifactId>
+    <version>1.0.0</version>
+    
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>3.8.1</version>
+        </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>module-b</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-core</artifactId>
+            <version>${spring.version}</version>
+        </dependency>
+    </dependencies>
+</project>"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("pom.xml");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = MavenParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        // Should only include junit, other dependencies have unresolved variables
+        assert_eq!(result.len(), 1);
+        
+        let junit = result.iter()
+            .find(|dep| dep.name == "junit:junit")
+            .expect("JUnit dependency not found");
+        assert_eq!(junit.version, "3.8.1");
+    }
+
+    #[tokio::test]
+    async fn test_maven_parser_missing_required_fields() {
+        let content = r#"<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0">
+    <modelVersion>4.0.0</modelVersion>
+    
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>3.8.1</version>
+        </dependency>
+        <dependency>
+            <artifactId>incomplete</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.example</groupId>
+            <version>2.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.example</groupId>
+            <artifactId>no-version</artifactId>
+        </dependency>
+    </dependencies>
+</project>"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("pom.xml");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = MavenParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        // Should only include junit (complete dependency)
+        assert_eq!(result.len(), 1);
+        
+        let junit = result.iter()
+            .find(|dep| dep.name == "junit:junit")
+            .expect("JUnit dependency not found");
+        assert_eq!(junit.version, "3.8.1");
+    }
+
+    #[tokio::test]
+    async fn test_maven_parser_with_additional_metadata() {
+        let content = r#"<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0">
+    <modelVersion>4.0.0</modelVersion>
+    
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.12.0</version>
+            <type>jar</type>
+            <scope>compile</scope>
+            <optional>false</optional>
+            <classifier>sources</classifier>
+        </dependency>
+    </dependencies>
+</project>"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("pom.xml");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = MavenParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 1);
+        
+        let commons = &result.iter().next().unwrap();
+        assert_eq!(commons.name, "org.apache.commons:commons-lang3");
+        assert_eq!(commons.version, "3.12.0");
+        assert_eq!(commons.metadata.get("type"), Some(&"jar".to_string()));
+        assert_eq!(commons.metadata.get("scope"), Some(&"compile".to_string()));
+        assert_eq!(commons.metadata.get("optional"), Some(&"false".to_string()));
+        assert_eq!(commons.metadata.get("classifier"), Some(&"sources".to_string()));
+    }
+}
\ No newline at end of file
src/parsers/javascript/mod.rs
@@ -0,0 +1,8 @@
+pub mod npm;
+pub mod yarn;
+
+#[cfg(test)]
+mod tests;
+
+pub use npm::NpmParser;
+pub use yarn::YarnParser;
\ No newline at end of file
src/parsers/javascript/npm.rs
@@ -0,0 +1,121 @@
+use crate::core::{
+    parser::{Parser, ParserError, ParserResult},
+    Dependency, DependencyCollection,
+};
+use async_trait::async_trait;
+use camino::Utf8Path;
+use serde_json::Value;
+use std::collections::HashMap;
+use tracing::debug;
+
+#[derive(Debug)]
+pub struct NpmParser;
+
+impl NpmParser {
+    pub fn new() -> Self {
+        Self
+    }
+    
+    fn matches_filename(&self, filename: &str) -> bool {
+        filename == "package-lock.json"
+    }
+}
+
+impl Default for NpmParser {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[async_trait]
+impl Parser for NpmParser {
+    fn can_parse(&self, path: &Utf8Path) -> bool {
+        path.file_name()
+            .map(|name| self.matches_filename(name))
+            .unwrap_or(false)
+    }
+    
+    fn name(&self) -> &'static str {
+        "npm"
+    }
+    
+    fn file_patterns(&self) -> Vec<&'static str> {
+        vec!["package-lock.json"]
+    }
+
+    async fn parse(&self, path: &Utf8Path) -> ParserResult<DependencyCollection> {
+        debug!("Parsing package-lock.json at: {}", path);
+        
+        let content = tokio::fs::read_to_string(path)
+            .await
+            .map_err(|_| ParserError::FileNotFound(path.to_path_buf()))?;
+        
+        let package_lock: Value = serde_json::from_str(&content)
+            .map_err(ParserError::JsonError)?;
+        
+        let mut dependencies = DependencyCollection::new();
+        
+        if let Some(deps) = package_lock.get("dependencies").and_then(|v| v.as_object()) {
+            for (name, metadata) in deps {
+                if let Some(dependency) = self.create_dependency(path, name, metadata)? {
+                    dependencies.add(dependency);
+                }
+            }
+        }
+        
+        debug!("Found {} dependencies in package-lock.json", dependencies.len());
+        Ok(dependencies)
+    }
+}
+
+impl NpmParser {
+    fn create_dependency(
+        &self,
+        path: &Utf8Path,
+        name: &str,
+        metadata: &Value,
+    ) -> ParserResult<Option<Dependency>> {
+        let version = metadata
+            .get("version")
+            .and_then(|v| v.as_str())
+            .unwrap_or("")
+            .to_string();
+        
+        if version.is_empty() {
+            return Ok(None);
+        }
+        
+        let mut meta = HashMap::new();
+        
+        // Extract resolved URL
+        if let Some(resolved) = metadata.get("resolved").and_then(|v| v.as_str()) {
+            meta.insert("resolved".to_string(), resolved.to_string());
+        }
+        
+        // Extract integrity hash
+        if let Some(integrity) = metadata.get("integrity").and_then(|v| v.as_str()) {
+            meta.insert("integrity".to_string(), integrity.to_string());
+        }
+        
+        // Extract dev flag
+        if let Some(dev) = metadata.get("dev").and_then(|v| v.as_bool()) {
+            meta.insert("dev".to_string(), dev.to_string());
+        }
+        
+        // Extract optional flag
+        if let Some(optional) = metadata.get("optional").and_then(|v| v.as_bool()) {
+            meta.insert("optional".to_string(), optional.to_string());
+        }
+        
+        // Extract bundled flag
+        if let Some(bundled) = metadata.get("bundled").and_then(|v| v.as_bool()) {
+            meta.insert("bundled".to_string(), bundled.to_string());
+        }
+        
+        let mut dependency = Dependency::new(name.to_string(), version);
+        dependency.location = path.to_path_buf();
+        dependency.metadata = meta;
+        
+        Ok(Some(dependency))
+    }
+}
\ No newline at end of file
src/parsers/javascript/tests.rs
@@ -0,0 +1,219 @@
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::core::parser::Parser;
+    use crate::parsers::{NpmParser, YarnParser};
+    use camino::Utf8PathBuf;
+    use std::fs;
+    use tempfile::tempdir;
+
+    #[tokio::test]
+    async fn test_npm_parser_can_parse() {
+        let parser = NpmParser::new();
+        
+        assert!(parser.can_parse(Utf8PathBuf::from("/path/to/package-lock.json").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/yarn.lock").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/package.json").as_path()));
+    }
+
+    #[tokio::test]
+    async fn test_npm_parser_parse_basic() {
+        let content = r#"{
+            "name": "test-project",
+            "version": "1.0.0",
+            "lockfileVersion": 1,
+            "requires": true,
+            "dependencies": {
+                "express": {
+                    "version": "4.17.1",
+                    "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
+                    "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
+                    "requires": {
+                        "accepts": "~1.3.7",
+                        "array-flatten": "1.1.1"
+                    }
+                },
+                "lodash": {
+                    "version": "4.17.21",
+                    "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+                    "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+                    "dev": true
+                }
+            }
+        }"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("package-lock.json");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = NpmParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 2);
+        
+        let express = result.iter()
+            .find(|dep| dep.name == "express")
+            .expect("Express dependency not found");
+        assert_eq!(express.version, "4.17.1");
+        assert_eq!(express.metadata.get("resolved"), Some(&"https://registry.npmjs.org/express/-/express-4.17.1.tgz".to_string()));
+        
+        let lodash = result.iter()
+            .find(|dep| dep.name == "lodash")
+            .expect("Lodash dependency not found");
+        assert_eq!(lodash.version, "4.17.21");
+        assert_eq!(lodash.metadata.get("dev"), Some(&"true".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_yarn_parser_can_parse() {
+        let parser = YarnParser::new();
+        
+        assert!(parser.can_parse(Utf8PathBuf::from("/path/to/yarn.lock").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/package-lock.json").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/package.json").as_path()));
+    }
+
+    #[tokio::test]
+    async fn test_yarn_parser_parse_basic() {
+        let content = r#"# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+"@babel/core@^7.8.4":
+  version "7.8.4"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.4.tgz#d496799e5c12195b3602d0fddd77294e3e38e80e"
+  integrity sha512-0LiLrB2PwrVI+a2/IEskBopDYSd8BCb3rOvH7D5tzoWd696TBEduBvuLVm4Nx6rltrLZqvI3MCalB2K2aVzQjA==
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/generator" "^7.8.4"
+
+lodash@^4.17.13:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("yarn.lock");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = YarnParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 2);
+        
+        let babel_core = result.iter()
+            .find(|dep| dep.name == "@babel/core")
+            .expect("@babel/core dependency not found");
+        assert_eq!(babel_core.version, "7.8.4");
+        assert_eq!(babel_core.metadata.get("resolved"), Some(&"https://registry.yarnpkg.com/@babel/core/-/core-7.8.4.tgz#d496799e5c12195b3602d0fddd77294e3e38e80e".to_string()));
+        
+        let lodash = result.iter()
+            .find(|dep| dep.name == "lodash")
+            .expect("Lodash dependency not found");
+        assert_eq!(lodash.version, "4.17.21");
+        assert_eq!(lodash.metadata.get("integrity"), Some(&"sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_yarn_parser_with_quoted_names() {
+        let content = r#"# yarn lockfile v1
+
+"@types/node@^14.0.0":
+  version "14.18.63"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz"
+  integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==
+
+"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("yarn.lock");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = YarnParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 1);
+        
+        let types_node = result.iter()
+            .find(|dep| dep.name == "@types/node")
+            .expect("@types/node dependency not found");
+        assert_eq!(types_node.version, "14.18.63");
+    }
+
+    #[tokio::test]
+    async fn test_yarn_parser_empty_dependencies() {
+        let content = r#"# yarn lockfile v1
+
+"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("yarn.lock");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = YarnParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 0);
+    }
+
+    #[tokio::test]
+    async fn test_npm_parser_empty_dependencies() {
+        let content = r#"{
+            "name": "test-project",
+            "version": "1.0.0",
+            "lockfileVersion": 1,
+            "requires": true,
+            "dependencies": {}
+        }"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("package-lock.json");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = NpmParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 0);
+    }
+
+    #[tokio::test]
+    async fn test_npm_parser_missing_version() {
+        let content = r#"{
+            "name": "test-project",
+            "version": "1.0.0",
+            "lockfileVersion": 1,
+            "requires": true,
+            "dependencies": {
+                "express": {
+                    "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz"
+                },
+                "lodash": {
+                    "version": "4.17.21",
+                    "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
+                }
+            }
+        }"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("package-lock.json");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = NpmParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        // Should only include lodash, not express (missing version)
+        assert_eq!(result.len(), 1);
+        
+        let lodash = result.iter()
+            .find(|dep| dep.name == "lodash")
+            .expect("Lodash dependency not found");
+        assert_eq!(lodash.version, "4.17.21");
+    }
+}
\ No newline at end of file
src/parsers/javascript/yarn.rs
@@ -0,0 +1,160 @@
+use crate::core::{
+    parser::{Parser, ParserError, ParserResult},
+    Dependency, DependencyCollection,
+};
+use async_trait::async_trait;
+use camino::Utf8Path;
+use regex::Regex;
+use std::collections::HashMap;
+use tokio::io::{AsyncBufReadExt, BufReader};
+use tracing::debug;
+
+#[derive(Debug)]
+pub struct YarnParser {
+    start_regex: Regex,
+    inject_colon: Regex,
+}
+
+impl YarnParser {
+    pub fn new() -> Self {
+        Self {
+            start_regex: Regex::new(r#"^"?(?P<name>(?:@|[\w\-\./])+)@"#).unwrap(),
+            inject_colon: Regex::new(r#"(\w|")\s(\w|")"#).unwrap(),
+        }
+    }
+}
+
+impl Default for YarnParser {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl YarnParser {
+    fn matches_filename(&self, filename: &str) -> bool {
+        filename == "yarn.lock"
+    }
+}
+
+#[async_trait]
+impl Parser for YarnParser {
+    fn can_parse(&self, path: &Utf8Path) -> bool {
+        path.file_name()
+            .map(|name| self.matches_filename(name))
+            .unwrap_or(false)
+    }
+    
+    fn name(&self) -> &'static str {
+        "yarn"
+    }
+    
+    fn file_patterns(&self) -> Vec<&'static str> {
+        vec!["yarn.lock"]
+    }
+
+    async fn parse(&self, path: &Utf8Path) -> ParserResult<DependencyCollection> {
+        debug!("Parsing yarn.lock at: {}", path);
+        
+        let file = tokio::fs::File::open(path)
+            .await
+            .map_err(|_| ParserError::FileNotFound(path.to_path_buf()))?;
+        
+        let reader = BufReader::new(file);
+        let mut lines = reader.lines();
+        let mut dependencies = DependencyCollection::new();
+        
+        while let Some(line) = lines.next_line().await.map_err(ParserError::IoError)? {
+            if let Some(dependency) = self.parse_dependency_from_line(&line, &mut lines, path).await? {
+                dependencies.add(dependency);
+            }
+        }
+        
+        debug!("Found {} dependencies in yarn.lock", dependencies.len());
+        Ok(dependencies)
+    }
+}
+
+impl YarnParser {
+    async fn parse_dependency_from_line(
+        &self,
+        header: &str,
+        lines: &mut tokio::io::Lines<BufReader<tokio::fs::File>>,
+        path: &Utf8Path,
+    ) -> ParserResult<Option<Dependency>> {
+        let captures = match self.start_regex.captures(header) {
+            Some(caps) => caps,
+            None => return Ok(None),
+        };
+        
+        let name = captures
+            .name("name")
+            .map(|m| m.as_str().trim_matches('"'))
+            .unwrap_or("")
+            .to_string();
+        
+        if name.is_empty() {
+            return Ok(None);
+        }
+        
+        let dependency_lines = self.read_dependency_lines(lines).await?;
+        let metadata = self.parse_yaml_like_content(&name, &dependency_lines)?;
+        
+        let version = metadata
+            .get("version")
+            .cloned()
+            .unwrap_or_default();
+        
+        if version.is_empty() {
+            return Ok(None);
+        }
+        
+        let mut dependency = Dependency::new(name, version);
+        dependency.location = path.to_path_buf();
+        dependency.metadata = metadata;
+        
+        Ok(Some(dependency))
+    }
+    
+    async fn read_dependency_lines(
+        &self,
+        lines: &mut tokio::io::Lines<BufReader<tokio::fs::File>>,
+    ) -> ParserResult<Vec<String>> {
+        let mut dependency_lines = Vec::new();
+        
+        while let Some(line) = lines.next_line().await.map_err(ParserError::IoError)? {
+            let trimmed = line.trim();
+            
+            if trimmed.is_empty() {
+                break;
+            }
+            
+            dependency_lines.push(trimmed.to_string());
+        }
+        
+        Ok(dependency_lines)
+    }
+    
+    fn parse_yaml_like_content(
+        &self,
+        name: &str,
+        lines: &[String],
+    ) -> ParserResult<HashMap<String, String>> {
+        let mut metadata = HashMap::new();
+        metadata.insert("name".to_string(), name.to_string());
+        
+        for line in lines {
+            let yaml_line = self.inject_colon.replace_all(line, "$1: $2");
+            
+            if let Some((key, value)) = yaml_line.split_once(':') {
+                let key = key.trim().to_string();
+                let value = value.trim().trim_matches('"').to_string();
+                
+                if !key.is_empty() && !value.is_empty() {
+                    metadata.insert(key, value);
+                }
+            }
+        }
+        
+        Ok(metadata)
+    }
+}
\ No newline at end of file
src/parsers/os/apk.rs
@@ -0,0 +1,135 @@
+use crate::core::{
+    parser::{Parser, ParserError, ParserResult},
+    Dependency, DependencyCollection,
+};
+use async_trait::async_trait;
+use camino::Utf8Path;
+use std::collections::HashMap;
+use tracing::debug;
+
+#[derive(Debug)]
+pub struct ApkParser;
+
+impl ApkParser {
+    pub fn new() -> Self {
+        Self
+    }
+    
+    fn matches_filename(&self, filename: &str) -> bool {
+        filename == "installed"
+    }
+}
+
+impl Default for ApkParser {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[async_trait]
+impl Parser for ApkParser {
+    fn can_parse(&self, path: &Utf8Path) -> bool {
+        path.file_name()
+            .map(|name| self.matches_filename(name))
+            .unwrap_or(false)
+    }
+    
+    fn name(&self) -> &'static str {
+        "apk"
+    }
+    
+    fn file_patterns(&self) -> Vec<&'static str> {
+        vec!["installed"]
+    }
+
+    async fn parse(&self, path: &Utf8Path) -> ParserResult<DependencyCollection> {
+        debug!("Parsing APK installed file at: {}", path);
+        
+        let content = tokio::fs::read_to_string(path)
+            .await
+            .map_err(|_| ParserError::FileNotFound(path.to_path_buf()))?;
+        
+        let mut dependencies = DependencyCollection::new();
+        let mut current_package = HashMap::new();
+        
+        for line in content.lines() {
+            if line.trim().is_empty() {
+                // End of package, create dependency if we have the required fields
+                if let Some(dependency) = self.create_dependency_from_package(path, &current_package)? {
+                    dependencies.add(dependency);
+                }
+                current_package.clear();
+            } else {
+                // Parse key:value line
+                if let Some((key, value)) = line.split_once(':') {
+                    current_package.insert(key.to_string(), value.to_string());
+                }
+            }
+        }
+        
+        // Handle last package if file doesn't end with empty line
+        if !current_package.is_empty() {
+            if let Some(dependency) = self.create_dependency_from_package(path, &current_package)? {
+                dependencies.add(dependency);
+            }
+        }
+        
+        debug!("Found {} dependencies in APK installed file", dependencies.len());
+        Ok(dependencies)
+    }
+}
+
+impl ApkParser {
+    fn create_dependency_from_package(
+        &self,
+        path: &Utf8Path,
+        package: &HashMap<String, String>,
+    ) -> ParserResult<Option<Dependency>> {
+        // Extract package name (P field)
+        let package_name = package.get("P")
+            .cloned()
+            .unwrap_or_default();
+        
+        // Extract version (V field)
+        let version = package.get("V")
+            .cloned()
+            .unwrap_or_default();
+        
+        if package_name.is_empty() || version.is_empty() {
+            return Ok(None);
+        }
+        
+        let mut meta = HashMap::new();
+        
+        // Store all APK fields as metadata
+        for (key, value) in package {
+            match key.as_str() {
+                "P" => {}, // Package name, already used
+                "V" => {}, // Version, already used
+                "C" => { meta.insert("checksum".to_string(), value.clone()); },
+                "A" => { meta.insert("architecture".to_string(), value.clone()); },
+                "S" => { meta.insert("size".to_string(), value.clone()); },
+                "I" => { meta.insert("installed_size".to_string(), value.clone()); },
+                "T" => { meta.insert("description".to_string(), value.clone()); },
+                "U" => { meta.insert("url".to_string(), value.clone()); },
+                "L" => { meta.insert("license".to_string(), value.clone()); },
+                "o" => { meta.insert("origin".to_string(), value.clone()); },
+                "m" => { meta.insert("maintainer".to_string(), value.clone()); },
+                "t" => { meta.insert("build_time".to_string(), value.clone()); },
+                "D" => { meta.insert("depends".to_string(), value.clone()); },
+                "p" => { meta.insert("provides".to_string(), value.clone()); },
+                "r" => { meta.insert("replaces".to_string(), value.clone()); },
+                "i" => { meta.insert("install_if".to_string(), value.clone()); },
+                _ => {
+                    meta.insert(key.clone(), value.clone());
+                }
+            }
+        }
+        
+        let mut dependency = Dependency::new(package_name, version);
+        dependency.location = path.to_path_buf();
+        dependency.metadata = meta;
+        
+        Ok(Some(dependency))
+    }
+}
\ No newline at end of file
src/parsers/os/dpkg.rs
@@ -0,0 +1,183 @@
+use crate::core::{
+    parser::{Parser, ParserError, ParserResult},
+    Dependency, DependencyCollection,
+};
+use async_trait::async_trait;
+use camino::Utf8Path;
+use std::collections::HashMap;
+use tracing::debug;
+
+#[derive(Debug)]
+pub struct DpkgParser;
+
+impl DpkgParser {
+    pub fn new() -> Self {
+        Self
+    }
+    
+    fn matches_filename(&self, filename: &str) -> bool {
+        filename == "status"
+    }
+}
+
+impl Default for DpkgParser {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[async_trait]
+impl Parser for DpkgParser {
+    fn can_parse(&self, path: &Utf8Path) -> bool {
+        path.file_name()
+            .map(|name| self.matches_filename(name))
+            .unwrap_or(false)
+    }
+    
+    fn name(&self) -> &'static str {
+        "dpkg"
+    }
+    
+    fn file_patterns(&self) -> Vec<&'static str> {
+        vec!["status"]
+    }
+
+    async fn parse(&self, path: &Utf8Path) -> ParserResult<DependencyCollection> {
+        debug!("Parsing DPKG status file at: {}", path);
+        
+        let content = tokio::fs::read_to_string(path)
+            .await
+            .map_err(|_| ParserError::FileNotFound(path.to_path_buf()))?;
+        
+        let mut dependencies = DependencyCollection::new();
+        let mut current_package = HashMap::new();
+        let mut lines = content.lines().peekable();
+        
+        while let Some(line) = lines.next() {
+            if line.trim().is_empty() {
+                // End of package, create dependency if we have the required fields
+                if let Some(dependency) = self.create_dependency_from_package(path, &current_package)? {
+                    dependencies.add(dependency);
+                }
+                current_package.clear();
+            } else {
+                // Parse Debian control format
+                self.parse_control_line(line, &mut lines, &mut current_package);
+            }
+        }
+        
+        // Handle last package if file doesn't end with empty line
+        if !current_package.is_empty() {
+            if let Some(dependency) = self.create_dependency_from_package(path, &current_package)? {
+                dependencies.add(dependency);
+            }
+        }
+        
+        debug!("Found {} dependencies in DPKG status file", dependencies.len());
+        Ok(dependencies)
+    }
+}
+
+impl DpkgParser {
+    fn parse_control_line(
+        &self,
+        line: &str,
+        lines: &mut std::iter::Peekable<std::str::Lines>,
+        package: &mut HashMap<String, String>,
+    ) {
+        if line.starts_with(' ') || line.starts_with('\t') {
+            // Continuation line - find the last key and append
+            if let Some((last_key, _)) = package.iter().last() {
+                let last_key = last_key.clone();
+                let existing_value = package.get(&last_key).unwrap_or(&String::new()).clone();
+                let new_value = if existing_value.is_empty() {
+                    line.to_string()
+                } else {
+                    format!("{}\n{}", existing_value, line)
+                };
+                package.insert(last_key, new_value);
+            }
+        } else if let Some((key, value)) = line.split_once(':') {
+            let key = key.trim().to_string();
+            let mut value = value.trim().to_string();
+            
+            // Handle multi-line values by reading continuation lines
+            while let Some(next_line) = lines.peek() {
+                if next_line.starts_with(' ') || next_line.starts_with('\t') {
+                    let continuation = lines.next().unwrap();
+                    value.push('\n');
+                    value.push_str(continuation);
+                } else {
+                    break;
+                }
+            }
+            
+            package.insert(key, value);
+        }
+    }
+    
+    fn create_dependency_from_package(
+        &self,
+        path: &Utf8Path,
+        package: &HashMap<String, String>,
+    ) -> ParserResult<Option<Dependency>> {
+        // Extract package name
+        let package_name = package.get("Package")
+            .cloned()
+            .unwrap_or_default();
+        
+        // Extract version
+        let version = package.get("Version")
+            .cloned()
+            .unwrap_or_default();
+        
+        if package_name.is_empty() || version.is_empty() {
+            return Ok(None);
+        }
+        
+        // Check if package is installed (not just configured)
+        if let Some(status) = package.get("Status") {
+            if !status.contains("install ok installed") {
+                return Ok(None); // Skip packages that aren't fully installed
+            }
+        }
+        
+        let mut meta = HashMap::new();
+        
+        // Store all DPKG fields as metadata
+        for (key, value) in package {
+            match key.as_str() {
+                "Package" => {}, // Package name, already used
+                "Version" => {}, // Version, already used
+                "Status" => { meta.insert("status".to_string(), value.clone()); },
+                "Priority" => { meta.insert("priority".to_string(), value.clone()); },
+                "Section" => { meta.insert("section".to_string(), value.clone()); },
+                "Installed-Size" => { meta.insert("installed_size".to_string(), value.clone()); },
+                "Maintainer" => { meta.insert("maintainer".to_string(), value.clone()); },
+                "Architecture" => { meta.insert("architecture".to_string(), value.clone()); },
+                "Multi-Arch" => { meta.insert("multi_arch".to_string(), value.clone()); },
+                "Depends" => { meta.insert("depends".to_string(), value.clone()); },
+                "Pre-Depends" => { meta.insert("pre_depends".to_string(), value.clone()); },
+                "Recommends" => { meta.insert("recommends".to_string(), value.clone()); },
+                "Suggests" => { meta.insert("suggests".to_string(), value.clone()); },
+                "Conflicts" => { meta.insert("conflicts".to_string(), value.clone()); },
+                "Breaks" => { meta.insert("breaks".to_string(), value.clone()); },
+                "Replaces" => { meta.insert("replaces".to_string(), value.clone()); },
+                "Provides" => { meta.insert("provides".to_string(), value.clone()); },
+                "Description" => { meta.insert("description".to_string(), value.clone()); },
+                "Homepage" => { meta.insert("homepage".to_string(), value.clone()); },
+                "Source" => { meta.insert("source".to_string(), value.clone()); },
+                "Essential" => { meta.insert("essential".to_string(), value.clone()); },
+                _ => {
+                    meta.insert(key.clone(), value.clone());
+                }
+            }
+        }
+        
+        let mut dependency = Dependency::new(package_name, version);
+        dependency.location = path.to_path_buf();
+        dependency.metadata = meta;
+        
+        Ok(Some(dependency))
+    }
+}
\ No newline at end of file
src/parsers/os/mod.rs
@@ -0,0 +1,8 @@
+pub mod apk;
+pub mod dpkg;
+
+#[cfg(test)]
+mod tests;
+
+pub use apk::ApkParser;
+pub use dpkg::DpkgParser;
\ No newline at end of file
src/parsers/os/tests.rs
@@ -0,0 +1,280 @@
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::core::parser::Parser;
+    use crate::parsers::{ApkParser, DpkgParser};
+    use camino::Utf8PathBuf;
+    use std::fs;
+    use tempfile::tempdir;
+
+    #[test]
+    fn test_apk_parser_can_parse() {
+        let parser = ApkParser::new();
+        
+        assert!(parser.can_parse(Utf8PathBuf::from("/lib/apk/db/installed").as_path()));
+        assert!(parser.can_parse(Utf8PathBuf::from("/path/to/installed").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/var/lib/dpkg/status").as_path()));
+    }
+
+    #[test]
+    fn test_dpkg_parser_can_parse() {
+        let parser = DpkgParser::new();
+        
+        assert!(parser.can_parse(Utf8PathBuf::from("/var/lib/dpkg/status").as_path()));
+        assert!(parser.can_parse(Utf8PathBuf::from("/path/to/status").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/lib/apk/db/installed").as_path()));
+    }
+
+    #[tokio::test]
+    async fn test_apk_parser_parse_basic() {
+        let content = r#"C:Q1SJUcZmtG6o3F1bu1Pfo7HuBsGwY=
+P:musl
+V:1.1.24-r9
+A:x86_64
+S:377256
+I:614400
+T:the musl c library (libc) implementation
+U:https://musl.libc.org/
+L:MIT
+o:musl
+m:Timo Terรคs <timo.teras@iki.fi>
+t:1592662716
+
+C:Q1abc123def456
+P:busybox
+V:1.32.0-r8
+A:x86_64
+S:924672
+I:1851392
+T:Swiss Army Knife of Embedded Linux
+U:https://busybox.net/
+L:GPL-2.0-only
+o:busybox
+m:Sรถren Tempel <soeren+alpine@soeren-tempel.net>
+t:1592662800
+"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("installed");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = ApkParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 2);
+        
+        // Check musl package
+        let musl = result.iter()
+            .find(|dep| dep.name == "musl")
+            .expect("musl package not found");
+        assert_eq!(musl.version, "1.1.24-r9");
+        assert_eq!(musl.metadata.get("architecture"), Some(&"x86_64".to_string()));
+        assert_eq!(musl.metadata.get("license"), Some(&"MIT".to_string()));
+        assert_eq!(musl.metadata.get("description"), Some(&"the musl c library (libc) implementation".to_string()));
+        assert_eq!(musl.metadata.get("url"), Some(&"https://musl.libc.org/".to_string()));
+        
+        // Check busybox package
+        let busybox = result.iter()
+            .find(|dep| dep.name == "busybox")
+            .expect("busybox package not found");
+        assert_eq!(busybox.version, "1.32.0-r8");
+        assert_eq!(busybox.metadata.get("license"), Some(&"GPL-2.0-only".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_dpkg_parser_parse_basic() {
+        let content = r#"Package: adduser
+Status: install ok installed
+Priority: important
+Section: admin
+Installed-Size: 849
+Maintainer: Debian Adduser Developers <adduser@packages.debian.org>
+Architecture: all
+Multi-Arch: foreign
+Version: 3.118
+Depends: passwd, debconf (>= 0.5) | debconf-2.0
+Suggests: liblocale-gettext-perl, perl
+Description: add and remove users and groups
+ This package includes the 'adduser' and 'deluser' commands for creating
+ and removing users.
+ .
+ With the standard Debian policy, UIDs from 1000 upwards are intended for
+ regular users, and UIDs from 100-999 for services.
+
+Package: base-files
+Status: install ok installed
+Priority: required
+Section: admin
+Installed-Size: 384
+Maintainer: Santiago Vila <sanvila@debian.org>
+Architecture: amd64
+Multi-Arch: foreign
+Version: 11.1+deb11u5
+Replaces: base
+Provides: base
+Conflicts: base
+Description: Debian base system miscellaneous files
+ This package contains the basic filesystem hierarchy of a Debian system, and
+ several important miscellaneous files, such as /etc/debian_version,
+ /etc/host.conf, /etc/issue, /etc/motd, /etc/profile, and others,
+ and the text of several common licenses in use on Debian systems.
+"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("status");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = DpkgParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 2);
+        
+        // Check adduser package
+        let adduser = result.iter()
+            .find(|dep| dep.name == "adduser")
+            .expect("adduser package not found");
+        assert_eq!(adduser.version, "3.118");
+        assert_eq!(adduser.metadata.get("priority"), Some(&"important".to_string()));
+        assert_eq!(adduser.metadata.get("section"), Some(&"admin".to_string()));
+        assert_eq!(adduser.metadata.get("architecture"), Some(&"all".to_string()));
+        assert_eq!(adduser.metadata.get("depends"), Some(&"passwd, debconf (>= 0.5) | debconf-2.0".to_string()));
+        assert!(adduser.metadata.get("description").unwrap().contains("add and remove users and groups"));
+        
+        // Check base-files package
+        let base_files = result.iter()
+            .find(|dep| dep.name == "base-files")
+            .expect("base-files package not found");
+        assert_eq!(base_files.version, "11.1+deb11u5");
+        assert_eq!(base_files.metadata.get("architecture"), Some(&"amd64".to_string()));
+        assert_eq!(base_files.metadata.get("provides"), Some(&"base".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_apk_parser_empty_file() {
+        let content = "";
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("installed");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = ApkParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 0);
+    }
+
+    #[tokio::test]
+    async fn test_dpkg_parser_empty_file() {
+        let content = "";
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("status");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = DpkgParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 0);
+    }
+
+    #[tokio::test]
+    async fn test_apk_parser_missing_required_fields() {
+        let content = r#"C:Q1SJUcZmtG6o3F1bu1Pfo7HuBsGwY=
+P:musl
+V:1.1.24-r9
+A:x86_64
+
+C:Q2abc123def456
+P:invalid-package
+A:x86_64
+
+P:another-invalid
+"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("installed");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = ApkParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        // Should only include musl (complete package)
+        assert_eq!(result.len(), 1);
+        
+        let musl = result.iter()
+            .find(|dep| dep.name == "musl")
+            .expect("musl package not found");
+        assert_eq!(musl.version, "1.1.24-r9");
+    }
+
+    #[tokio::test]
+    async fn test_dpkg_parser_not_installed_packages() {
+        let content = r#"Package: installed-package
+Status: install ok installed
+Version: 1.0.0
+Architecture: amd64
+
+Package: config-only-package
+Status: deinstall ok config-files
+Version: 2.0.0
+Architecture: amd64
+
+Package: half-configured-package
+Status: install ok half-configured
+Version: 3.0.0
+Architecture: amd64
+"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("status");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = DpkgParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        // Should only include the fully installed package
+        assert_eq!(result.len(), 1);
+        
+        let installed = result.iter()
+            .find(|dep| dep.name == "installed-package")
+            .expect("installed package not found");
+        assert_eq!(installed.version, "1.0.0");
+    }
+
+    #[tokio::test]
+    async fn test_dpkg_parser_multiline_description() {
+        let content = r#"Package: test-package
+Status: install ok installed
+Version: 1.0.0
+Architecture: amd64
+Description: A test package with multiline description
+ This is the first line of the extended description.
+ .
+ This is after a paragraph break.
+ This line continues the paragraph.
+"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("status");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = DpkgParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 1);
+        
+        let package = &result.iter().next().unwrap();
+        assert_eq!(package.name, "test-package");
+        let description = package.metadata.get("description").unwrap();
+        assert!(description.contains("A test package with multiline description"));
+        assert!(description.contains("This is the first line"));
+        assert!(description.contains("paragraph break"));
+    }
+}
\ No newline at end of file
src/parsers/php/composer.rs
@@ -0,0 +1,198 @@
+use crate::core::{
+    parser::{Parser, ParserError, ParserResult},
+    Dependency, DependencyCollection,
+};
+use async_trait::async_trait;
+use camino::Utf8Path;
+use serde_json::Value;
+use std::collections::HashMap;
+use tracing::debug;
+
+#[derive(Debug)]
+pub struct ComposerParser;
+
+impl ComposerParser {
+    pub fn new() -> Self {
+        Self
+    }
+    
+    fn matches_filename(&self, filename: &str) -> bool {
+        filename == "composer.lock"
+    }
+}
+
+impl Default for ComposerParser {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[async_trait]
+impl Parser for ComposerParser {
+    fn can_parse(&self, path: &Utf8Path) -> bool {
+        path.file_name()
+            .map(|name| self.matches_filename(name))
+            .unwrap_or(false)
+    }
+    
+    fn name(&self) -> &'static str {
+        "composer"
+    }
+    
+    fn file_patterns(&self) -> Vec<&'static str> {
+        vec!["composer.lock"]
+    }
+
+    async fn parse(&self, path: &Utf8Path) -> ParserResult<DependencyCollection> {
+        debug!("Parsing composer.lock at: {}", path);
+        
+        let content = tokio::fs::read_to_string(path)
+            .await
+            .map_err(|_| ParserError::FileNotFound(path.to_path_buf()))?;
+        
+        let composer_lock: Value = serde_json::from_str(&content)
+            .map_err(ParserError::JsonError)?;
+        
+        let mut dependencies = DependencyCollection::new();
+        
+        // Parse production packages
+        if let Some(packages) = composer_lock.get("packages").and_then(|v| v.as_array()) {
+            for package in packages {
+                if let Some(dependency) = self.create_dependency(path, package, "production")? {
+                    dependencies.add(dependency);
+                }
+            }
+        }
+        
+        // Parse development packages
+        if let Some(packages_dev) = composer_lock.get("packages-dev").and_then(|v| v.as_array()) {
+            for package in packages_dev {
+                if let Some(dependency) = self.create_dependency(path, package, "development")? {
+                    dependencies.add(dependency);
+                }
+            }
+        }
+        
+        debug!("Found {} dependencies in composer.lock", dependencies.len());
+        Ok(dependencies)
+    }
+}
+
+impl ComposerParser {
+    fn create_dependency(
+        &self,
+        path: &Utf8Path,
+        package: &Value,
+        group: &str,
+    ) -> ParserResult<Option<Dependency>> {
+        let name = package
+            .get("name")
+            .and_then(|v| v.as_str())
+            .unwrap_or("")
+            .to_string();
+        
+        let version = package
+            .get("version")
+            .and_then(|v| v.as_str())
+            .unwrap_or("")
+            .to_string();
+        
+        if name.is_empty() || version.is_empty() {
+            return Ok(None);
+        }
+        
+        let mut meta = HashMap::new();
+        
+        // Add group information
+        meta.insert("group".to_string(), group.to_string());
+        
+        // Extract type
+        if let Some(pkg_type) = package.get("type").and_then(|v| v.as_str()) {
+            meta.insert("type".to_string(), pkg_type.to_string());
+        }
+        
+        // Extract description
+        if let Some(description) = package.get("description").and_then(|v| v.as_str()) {
+            meta.insert("description".to_string(), description.to_string());
+        }
+        
+        // Extract homepage
+        if let Some(homepage) = package.get("homepage").and_then(|v| v.as_str()) {
+            meta.insert("homepage".to_string(), homepage.to_string());
+        }
+        
+        // Extract keywords
+        if let Some(keywords) = package.get("keywords").and_then(|v| v.as_array()) {
+            let keyword_strings: Vec<String> = keywords
+                .iter()
+                .filter_map(|k| k.as_str())
+                .map(|k| k.to_string())
+                .collect();
+            if !keyword_strings.is_empty() {
+                meta.insert("keywords".to_string(), keyword_strings.join(","));
+            }
+        }
+        
+        // Extract license information
+        if let Some(licenses) = package.get("license").and_then(|v| v.as_array()) {
+            let license_strings: Vec<String> = licenses
+                .iter()
+                .filter_map(|l| l.as_str())
+                .map(|l| l.to_string())
+                .collect();
+            if !license_strings.is_empty() {
+                meta.insert("license".to_string(), license_strings.join(","));
+            }
+        }
+        
+        // Extract source information
+        if let Some(source) = package.get("source").and_then(|v| v.as_object()) {
+            if let Some(url) = source.get("url").and_then(|v| v.as_str()) {
+                meta.insert("source_url".to_string(), url.to_string());
+            }
+            if let Some(reference) = source.get("reference").and_then(|v| v.as_str()) {
+                meta.insert("source_reference".to_string(), reference.to_string());
+            }
+            if let Some(source_type) = source.get("type").and_then(|v| v.as_str()) {
+                meta.insert("source_type".to_string(), source_type.to_string());
+            }
+        }
+        
+        // Extract distribution information
+        if let Some(dist) = package.get("dist").and_then(|v| v.as_object()) {
+            if let Some(url) = dist.get("url").and_then(|v| v.as_str()) {
+                meta.insert("dist_url".to_string(), url.to_string());
+            }
+            if let Some(shasum) = dist.get("shasum").and_then(|v| v.as_str()) {
+                meta.insert("dist_shasum".to_string(), shasum.to_string());
+            }
+            if let Some(dist_type) = dist.get("type").and_then(|v| v.as_str()) {
+                meta.insert("dist_type".to_string(), dist_type.to_string());
+            }
+        }
+        
+        // Extract authors
+        if let Some(authors) = package.get("authors").and_then(|v| v.as_array()) {
+            let author_names: Vec<String> = authors
+                .iter()
+                .filter_map(|a| a.as_object())
+                .filter_map(|a| a.get("name").and_then(|n| n.as_str()))
+                .map(|n| n.to_string())
+                .collect();
+            if !author_names.is_empty() {
+                meta.insert("authors".to_string(), author_names.join(","));
+            }
+        }
+        
+        // Extract time
+        if let Some(time) = package.get("time").and_then(|v| v.as_str()) {
+            meta.insert("time".to_string(), time.to_string());
+        }
+        
+        let mut dependency = Dependency::new(name, version);
+        dependency.location = path.to_path_buf();
+        dependency.metadata = meta;
+        
+        Ok(Some(dependency))
+    }
+}
\ No newline at end of file
src/parsers/php/mod.rs
@@ -0,0 +1,6 @@
+pub mod composer;
+
+#[cfg(test)]
+mod tests;
+
+pub use composer::ComposerParser;
\ No newline at end of file
src/parsers/php/tests.rs
@@ -0,0 +1,349 @@
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::core::parser::Parser;
+    use crate::parsers::ComposerParser;
+    use camino::Utf8PathBuf;
+    use std::fs;
+    use tempfile::tempdir;
+
+    #[test]
+    fn test_composer_parser_can_parse() {
+        let parser = ComposerParser::new();
+        
+        assert!(parser.can_parse(Utf8PathBuf::from("/path/to/composer.lock").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/package.json").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/composer.json").as_path()));
+    }
+
+    #[tokio::test]
+    async fn test_composer_parser_parse_basic() {
+        let content = r#"{
+            "_readme": [
+                "This file locks the dependencies of your project to a known state",
+                "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies"
+            ],
+            "content-hash": "28b2e9ae8de59b2b5b9e8a6b2c7b4b4b4b4b4b4b",
+            "packages": [
+                {
+                    "name": "symfony/polyfill-ctype",
+                    "version": "v1.14.0",
+                    "source": {
+                        "type": "git",
+                        "url": "https://github.com/symfony/polyfill-ctype.git",
+                        "reference": "fbdeaec0df06cf3d51c93de80c7eb76e271f5a38"
+                    },
+                    "dist": {
+                        "type": "zip",
+                        "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/fbdeaec0df06cf3d51c93de80c7eb76e271f5a38",
+                        "reference": "fbdeaec0df06cf3d51c93de80c7eb76e271f5a38",
+                        "shasum": ""
+                    },
+                    "require": {
+                        "php": ">=5.3.3"
+                    },
+                    "suggest": {
+                        "ext-ctype": "For best performance"
+                    },
+                    "type": "library",
+                    "extra": {
+                        "branch-alias": {
+                            "dev-master": "1.14-dev"
+                        }
+                    },
+                    "autoload": {
+                        "psr-4": {
+                            "Symfony\\Polyfill\\Ctype\\": ""
+                        },
+                        "files": [
+                            "bootstrap.php"
+                        ]
+                    },
+                    "notification-url": "https://packagist.org/downloads/",
+                    "license": [
+                        "MIT"
+                    ],
+                    "authors": [
+                        {
+                            "name": "Gert de Pagter",
+                            "email": "BackEndTea@gmail.com"
+                        },
+                        {
+                            "name": "Symfony Community",
+                            "homepage": "https://symfony.com/contributors"
+                        }
+                    ],
+                    "description": "Symfony polyfill for ctype functions",
+                    "homepage": "https://symfony.com",
+                    "keywords": [
+                        "compatibility",
+                        "ctype",
+                        "polyfill",
+                        "portable"
+                    ],
+                    "time": "2020-01-13T11:15:53+00:00"
+                }
+            ],
+            "packages-dev": [
+                {
+                    "name": "mockery/mockery",
+                    "version": "1.3.1",
+                    "source": {
+                        "type": "git",
+                        "url": "https://github.com/mockery/mockery.git",
+                        "reference": "f69bbde7d7a75d6b2862d9ca8fab1cd28014b4be"
+                    },
+                    "dist": {
+                        "type": "zip",
+                        "url": "https://api.github.com/repos/mockery/mockery/zipball/f69bbde7d7a75d6b2862d9ca8fab1cd28014b4be",
+                        "reference": "f69bbde7d7a75d6b2862d9ca8fab1cd28014b4be",
+                        "shasum": ""
+                    },
+                    "require": {
+                        "hamcrest/hamcrest-php": "^2.0.1",
+                        "lib-pcre": ">=7.0",
+                        "php": ">=5.6.0"
+                    },
+                    "require-dev": {
+                        "phpunit/phpunit": "^5.7.10|^6.5|^7.0|^8.0"
+                    },
+                    "type": "library",
+                    "extra": {
+                        "branch-alias": {
+                            "dev-master": "1.3.x-dev"
+                        }
+                    },
+                    "autoload": {
+                        "psr-0": {
+                            "Mockery": "library/"
+                        }
+                    },
+                    "notification-url": "https://packagist.org/downloads/",
+                    "license": [
+                        "BSD-3-Clause"
+                    ],
+                    "authors": [
+                        {
+                            "name": "Pรกdraic Brady",
+                            "email": "padraic.brady@gmail.com",
+                            "homepage": "http://blog.astrumfutura.com"
+                        },
+                        {
+                            "name": "Dave Marshall",
+                            "email": "dave.marshall@atstsolutions.co.uk",
+                            "homepage": "http://davedevelopment.co.uk"
+                        }
+                    ],
+                    "description": "Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succint API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL).",
+                    "homepage": "https://github.com/mockery/mockery",
+                    "keywords": [
+                        "BDD",
+                        "TDD",
+                        "library",
+                        "mock",
+                        "mock objects",
+                        "mockery",
+                        "stub",
+                        "test",
+                        "test double",
+                        "testing"
+                    ],
+                    "time": "2019-12-26T09:49:15+00:00"
+                }
+            ],
+            "aliases": [],
+            "minimum-stability": "stable",
+            "stability-flags": [],
+            "prefer-stable": false,
+            "prefer-lowest": false,
+            "platform": {
+                "php": "^7.2"
+            },
+            "platform-dev": []
+        }"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("composer.lock");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = ComposerParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 2);
+        
+        // Check production package
+        let symfony = result.iter()
+            .find(|dep| dep.name == "symfony/polyfill-ctype")
+            .expect("Symfony package not found");
+        assert_eq!(symfony.version, "v1.14.0");
+        assert_eq!(symfony.metadata.get("group"), Some(&"production".to_string()));
+        assert_eq!(symfony.metadata.get("type"), Some(&"library".to_string()));
+        assert_eq!(symfony.metadata.get("license"), Some(&"MIT".to_string()));
+        assert_eq!(symfony.metadata.get("homepage"), Some(&"https://symfony.com".to_string()));
+        assert_eq!(symfony.metadata.get("keywords"), Some(&"compatibility,ctype,polyfill,portable".to_string()));
+        assert_eq!(symfony.metadata.get("authors"), Some(&"Gert de Pagter,Symfony Community".to_string()));
+        
+        // Check development package
+        let mockery = result.iter()
+            .find(|dep| dep.name == "mockery/mockery")
+            .expect("Mockery package not found");
+        assert_eq!(mockery.version, "1.3.1");
+        assert_eq!(mockery.metadata.get("group"), Some(&"development".to_string()));
+        assert_eq!(mockery.metadata.get("license"), Some(&"BSD-3-Clause".to_string()));
+        assert_eq!(mockery.metadata.get("homepage"), Some(&"https://github.com/mockery/mockery".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_composer_parser_empty_packages() {
+        let content = r#"{
+            "_readme": ["This file locks the dependencies"],
+            "content-hash": "28b2e9ae8de59b2b5b9e8a6b2c7b4b4b4b4b4b4b",
+            "packages": [],
+            "packages-dev": [],
+            "aliases": [],
+            "minimum-stability": "stable",
+            "platform": {}
+        }"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("composer.lock");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = ComposerParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 0);
+    }
+
+    #[tokio::test]
+    async fn test_composer_parser_missing_name_or_version() {
+        let content = r#"{
+            "packages": [
+                {
+                    "name": "valid/package",
+                    "version": "1.0.0",
+                    "type": "library"
+                },
+                {
+                    "version": "2.0.0",
+                    "type": "library"
+                },
+                {
+                    "name": "missing/version",
+                    "type": "library"
+                }
+            ],
+            "packages-dev": []
+        }"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("composer.lock");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = ComposerParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        // Should only include the valid package
+        assert_eq!(result.len(), 1);
+        
+        let valid = result.iter()
+            .find(|dep| dep.name == "valid/package")
+            .expect("Valid package not found");
+        assert_eq!(valid.version, "1.0.0");
+    }
+
+    #[tokio::test]
+    async fn test_composer_parser_only_dev_packages() {
+        let content = r#"{
+            "packages": [],
+            "packages-dev": [
+                {
+                    "name": "phpunit/phpunit",
+                    "version": "9.5.0",
+                    "type": "library",
+                    "license": ["BSD-3-Clause"],
+                    "description": "The PHP Unit Testing framework."
+                }
+            ]
+        }"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("composer.lock");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = ComposerParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 1);
+        
+        let phpunit = result.iter()
+            .find(|dep| dep.name == "phpunit/phpunit")
+            .expect("PHPUnit package not found");
+        assert_eq!(phpunit.version, "9.5.0");
+        assert_eq!(phpunit.metadata.get("group"), Some(&"development".to_string()));
+        assert_eq!(phpunit.metadata.get("license"), Some(&"BSD-3-Clause".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_composer_parser_metadata_extraction() {
+        let content = r#"{
+            "packages": [
+                {
+                    "name": "test/package",
+                    "version": "1.0.0",
+                    "source": {
+                        "type": "git",
+                        "url": "https://github.com/test/package.git",
+                        "reference": "abc123"
+                    },
+                    "dist": {
+                        "type": "zip",
+                        "url": "https://api.github.com/repos/test/package/zipball/abc123",
+                        "reference": "abc123",
+                        "shasum": "def456"
+                    },
+                    "type": "library",
+                    "description": "A test package",
+                    "homepage": "https://example.com",
+                    "keywords": ["test", "example"],
+                    "license": ["MIT", "Apache-2.0"],
+                    "authors": [
+                        {
+                            "name": "John Doe",
+                            "email": "john@example.com"
+                        },
+                        {
+                            "name": "Jane Smith"
+                        }
+                    ],
+                    "time": "2021-01-01T12:00:00+00:00"
+                }
+            ],
+            "packages-dev": []
+        }"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("composer.lock");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = ComposerParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 1);
+        
+        let package = &result.iter().next().unwrap();
+        assert_eq!(package.metadata.get("source_url"), Some(&"https://github.com/test/package.git".to_string()));
+        assert_eq!(package.metadata.get("source_reference"), Some(&"abc123".to_string()));
+        assert_eq!(package.metadata.get("source_type"), Some(&"git".to_string()));
+        assert_eq!(package.metadata.get("dist_shasum"), Some(&"def456".to_string()));
+        assert_eq!(package.metadata.get("license"), Some(&"MIT,Apache-2.0".to_string()));
+        assert_eq!(package.metadata.get("keywords"), Some(&"test,example".to_string()));
+        assert_eq!(package.metadata.get("authors"), Some(&"John Doe,Jane Smith".to_string()));
+        assert_eq!(package.metadata.get("time"), Some(&"2021-01-01T12:00:00+00:00".to_string()));
+    }
+}
\ No newline at end of file
src/parsers/python/mod.rs
@@ -0,0 +1,6 @@
+pub mod pipfile_lock;
+
+#[cfg(test)]
+mod tests;
+
+pub use pipfile_lock::PipfileLockParser;
\ No newline at end of file
src/parsers/python/pipfile_lock.rs
@@ -0,0 +1,143 @@
+use crate::core::{
+    parser::{Parser, ParserError, ParserResult},
+    Dependency, DependencyCollection,
+};
+use async_trait::async_trait;
+use camino::Utf8Path;
+use serde_json::Value;
+use std::collections::HashMap;
+use tracing::debug;
+
+#[derive(Debug)]
+pub struct PipfileLockParser;
+
+impl PipfileLockParser {
+    pub fn new() -> Self {
+        Self
+    }
+    
+    fn matches_filename(&self, filename: &str) -> bool {
+        filename.starts_with("Pipfile") && filename.ends_with(".lock")
+    }
+}
+
+impl Default for PipfileLockParser {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[async_trait]
+impl Parser for PipfileLockParser {
+    fn can_parse(&self, path: &Utf8Path) -> bool {
+        path.file_name()
+            .map(|name| self.matches_filename(name))
+            .unwrap_or(false)
+    }
+    
+    fn name(&self) -> &'static str {
+        "pipfile"
+    }
+    
+    fn file_patterns(&self) -> Vec<&'static str> {
+        vec!["Pipfile.lock", "Pipfile*.lock"]
+    }
+
+    async fn parse(&self, path: &Utf8Path) -> ParserResult<DependencyCollection> {
+        debug!("Parsing Pipfile.lock at: {}", path);
+        
+        let content = tokio::fs::read_to_string(path)
+            .await
+            .map_err(|_| ParserError::FileNotFound(path.to_path_buf()))?;
+        
+        let pipfile_lock: Value = serde_json::from_str(&content)
+            .map_err(ParserError::JsonError)?;
+        
+        let mut dependencies = DependencyCollection::new();
+        
+        // Parse dependencies from both "default" and "develop" groups
+        let groups = ["default", "develop"];
+        for group in &groups {
+            if let Some(group_deps) = pipfile_lock.get(group).and_then(|v| v.as_object()) {
+                for (name, metadata) in group_deps {
+                    if let Some(dependency) = self.create_dependency(path, name, metadata, group)? {
+                        dependencies.add(dependency);
+                    }
+                }
+            }
+        }
+        
+        debug!("Found {} dependencies in Pipfile.lock", dependencies.len());
+        Ok(dependencies)
+    }
+}
+
+impl PipfileLockParser {
+    fn create_dependency(
+        &self,
+        path: &Utf8Path,
+        name: &str,
+        metadata: &Value,
+        group: &str,
+    ) -> ParserResult<Option<Dependency>> {
+        let version = metadata
+            .get("version")
+            .and_then(|v| v.as_str())
+            .map(|v| self.canonicalize_version(v))
+            .unwrap_or_default();
+        
+        if version.is_empty() {
+            return Ok(None);
+        }
+        
+        let mut meta = HashMap::new();
+        
+        // Add group information
+        meta.insert("group".to_string(), group.to_string());
+        
+        // Extract hashes
+        if let Some(hashes) = metadata.get("hashes").and_then(|v| v.as_array()) {
+            let hash_strings: Vec<String> = hashes
+                .iter()
+                .filter_map(|h| h.as_str())
+                .map(|h| h.to_string())
+                .collect();
+            if !hash_strings.is_empty() {
+                meta.insert("hashes".to_string(), hash_strings.join(","));
+            }
+        }
+        
+        // Extract index
+        if let Some(index) = metadata.get("index").and_then(|v| v.as_str()) {
+            meta.insert("index".to_string(), index.to_string());
+        }
+        
+        // Extract markers (environment markers)
+        if let Some(markers) = metadata.get("markers").and_then(|v| v.as_str()) {
+            meta.insert("markers".to_string(), markers.to_string());
+        }
+        
+        // Extract extras
+        if let Some(extras) = metadata.get("extras").and_then(|v| v.as_array()) {
+            let extra_strings: Vec<String> = extras
+                .iter()
+                .filter_map(|e| e.as_str())
+                .map(|e| e.to_string())
+                .collect();
+            if !extra_strings.is_empty() {
+                meta.insert("extras".to_string(), extra_strings.join(","));
+            }
+        }
+        
+        let mut dependency = Dependency::new(name.to_string(), version);
+        dependency.location = path.to_path_buf();
+        dependency.metadata = meta;
+        
+        Ok(Some(dependency))
+    }
+    
+    fn canonicalize_version(&self, version: &str) -> String {
+        // Remove == prefix from version string
+        version.strip_prefix("==").unwrap_or(version).to_string()
+    }
+}
\ No newline at end of file
src/parsers/python/tests.rs
@@ -0,0 +1,250 @@
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::core::parser::Parser;
+    use crate::parsers::PipfileLockParser;
+    use camino::Utf8PathBuf;
+    use std::fs;
+    use tempfile::tempdir;
+
+    #[test]
+    fn test_pipfile_lock_parser_can_parse() {
+        let parser = PipfileLockParser::new();
+        
+        assert!(parser.can_parse(Utf8PathBuf::from("/path/to/Pipfile.lock").as_path()));
+        assert!(parser.can_parse(Utf8PathBuf::from("/path/to/Pipfile-dev.lock").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/requirements.txt").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/setup.py").as_path()));
+    }
+
+    #[tokio::test]
+    async fn test_pipfile_lock_parser_parse_basic() {
+        let content = r#"{
+            "_meta": {
+                "hash": {
+                    "sha256": "d9b5cc506fc4feb9bf1ae7cadfd3737d5a0bd2b2d6c3fbcf0de3458bab34ad89"
+                },
+                "pipfile-spec": 6,
+                "requires": {
+                    "python_version": "3.8"
+                },
+                "sources": [
+                    {
+                        "name": "pypi",
+                        "url": "https://pypi.org/simple",
+                        "verify_ssl": true
+                    }
+                ]
+            },
+            "default": {
+                "requests": {
+                    "hashes": [
+                        "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
+                        "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
+                    ],
+                    "index": "pypi",
+                    "version": "==2.25.1"
+                },
+                "urllib3": {
+                    "hashes": [
+                        "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"
+                    ],
+                    "markers": "python_version >= '2.7'",
+                    "version": "==1.26.7"
+                }
+            },
+            "develop": {
+                "pytest": {
+                    "hashes": [
+                        "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"
+                    ],
+                    "index": "pypi",
+                    "version": "==6.2.4",
+                    "extras": ["dev"]
+                }
+            }
+        }"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("Pipfile.lock");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = PipfileLockParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 3);
+        
+        // Check requests dependency
+        let requests = result.iter()
+            .find(|dep| dep.name == "requests")
+            .expect("Requests dependency not found");
+        assert_eq!(requests.version, "2.25.1");
+        assert_eq!(requests.metadata.get("group"), Some(&"default".to_string()));
+        assert_eq!(requests.metadata.get("index"), Some(&"pypi".to_string()));
+        assert!(requests.metadata.get("hashes").is_some());
+        
+        // Check urllib3 dependency with markers
+        let urllib3 = result.iter()
+            .find(|dep| dep.name == "urllib3")
+            .expect("urllib3 dependency not found");
+        assert_eq!(urllib3.version, "1.26.7");
+        assert_eq!(urllib3.metadata.get("markers"), Some(&"python_version >= '2.7'".to_string()));
+        
+        // Check pytest dependency in develop group
+        let pytest = result.iter()
+            .find(|dep| dep.name == "pytest")
+            .expect("pytest dependency not found");
+        assert_eq!(pytest.version, "6.2.4");
+        assert_eq!(pytest.metadata.get("group"), Some(&"develop".to_string()));
+        assert_eq!(pytest.metadata.get("extras"), Some(&"dev".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_pipfile_lock_parser_empty_groups() {
+        let content = r#"{
+            "_meta": {
+                "hash": {
+                    "sha256": "d9b5cc506fc4feb9bf1ae7cadfd3737d5a0bd2b2d6c3fbcf0de3458bab34ad89"
+                },
+                "pipfile-spec": 6
+            },
+            "default": {},
+            "develop": {}
+        }"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("Pipfile.lock");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = PipfileLockParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 0);
+    }
+
+    #[tokio::test]
+    async fn test_pipfile_lock_parser_missing_version() {
+        let content = r#"{
+            "_meta": {
+                "hash": {
+                    "sha256": "d9b5cc506fc4feb9bf1ae7cadfd3737d5a0bd2b2d6c3fbcf0de3458bab34ad89"
+                }
+            },
+            "default": {
+                "requests": {
+                    "hashes": [
+                        "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd"
+                    ],
+                    "index": "pypi"
+                },
+                "urllib3": {
+                    "version": "==1.26.7",
+                    "index": "pypi"
+                }
+            },
+            "develop": {}
+        }"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("Pipfile.lock");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = PipfileLockParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        // Should only include urllib3, not requests (missing version)
+        assert_eq!(result.len(), 1);
+        
+        let urllib3 = result.iter()
+            .find(|dep| dep.name == "urllib3")
+            .expect("urllib3 dependency not found");
+        assert_eq!(urllib3.version, "1.26.7");
+    }
+
+    #[tokio::test]
+    async fn test_pipfile_lock_parser_version_canonicalization() {
+        let content = r#"{
+            "_meta": {
+                "hash": {
+                    "sha256": "d9b5cc506fc4feb9bf1ae7cadfd3737d5a0bd2b2d6c3fbcf0de3458bab34ad89"
+                }
+            },
+            "default": {
+                "package1": {
+                    "version": "==1.2.3"
+                },
+                "package2": {
+                    "version": "1.2.3"
+                },
+                "package3": {
+                    "version": ">=1.2.3"
+                }
+            },
+            "develop": {}
+        }"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("Pipfile.lock");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = PipfileLockParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 3);
+        
+        let package1 = result.iter()
+            .find(|dep| dep.name == "package1")
+            .expect("package1 not found");
+        assert_eq!(package1.version, "1.2.3"); // == stripped
+        
+        let package2 = result.iter()
+            .find(|dep| dep.name == "package2")
+            .expect("package2 not found");
+        assert_eq!(package2.version, "1.2.3"); // no change
+        
+        let package3 = result.iter()
+            .find(|dep| dep.name == "package3")
+            .expect("package3 not found");
+        assert_eq!(package3.version, ">=1.2.3"); // no change
+    }
+
+    #[tokio::test]
+    async fn test_pipfile_lock_parser_only_develop_group() {
+        let content = r#"{
+            "_meta": {
+                "hash": {
+                    "sha256": "d9b5cc506fc4feb9bf1ae7cadfd3737d5a0bd2b2d6c3fbcf0de3458bab34ad89"
+                }
+            },
+            "develop": {
+                "pytest": {
+                    "hashes": [
+                        "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"
+                    ],
+                    "index": "pypi",
+                    "version": "==6.2.4"
+                }
+            }
+        }"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join("Pipfile.lock");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = PipfileLockParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 1);
+        
+        let pytest = result.iter()
+            .find(|dep| dep.name == "pytest")
+            .expect("pytest dependency not found");
+        assert_eq!(pytest.version, "6.2.4");
+        assert_eq!(pytest.metadata.get("group"), Some(&"develop".to_string()));
+    }
+}
\ No newline at end of file
src/parsers/ruby/gemfile_lock.rs
@@ -0,0 +1,352 @@
+use async_trait::async_trait;
+use camino::Utf8Path;
+use regex::Regex;
+use tracing::{debug, warn};
+
+use crate::core::{
+    parser::{Parser, ParserError, ParserResult},
+    Dependency, DependencyCollection,
+};
+
+#[derive(Debug)]
+pub struct GemfileLockParser {
+    strip_bundled_with: Regex,
+}
+
+impl GemfileLockParser {
+    pub fn new() -> Self {
+        Self {
+            strip_bundled_with: Regex::new(r"(?m)^BUNDLED WITH$\r?\n   \d+\.\d+\.\d+\r?\n?")
+                .expect("Invalid regex pattern"),
+        }
+    }
+
+    fn matches_filename(&self, filename: &str) -> bool {
+        filename.starts_with("Gemfile") && filename.ends_with(".lock")
+            || filename.starts_with("gems") && filename.ends_with(".lock")
+    }
+
+    async fn parse_gemfile_content(&self, content: &str, file_path: &Utf8Path) -> ParserResult<DependencyCollection> {
+        debug!("Parsing Gemfile.lock content, {} bytes", content.len());
+
+        // Remove BUNDLED WITH section that can interfere with parsing
+        let cleaned_content = self.strip_bundled_with.replace_all(content, "");
+        
+        let mut dependencies = DependencyCollection::new();
+        let parsed_data = self.parse_lockfile_format(&cleaned_content)?;
+
+        for spec in parsed_data.specs {
+            let dependency = Dependency::new(spec.name.clone(), spec.version.clone())
+                .with_location(file_path.to_path_buf())
+                .with_source("rubygems".to_string())
+                .add_metadata("platform".to_string(), spec.platform.clone())
+                .add_metadata("source".to_string(), spec.source.clone());
+
+            dependencies.add(dependency);
+        }
+
+        debug!("Parsed {} dependencies from {}", dependencies.len(), file_path);
+        Ok(dependencies)
+    }
+
+    fn parse_lockfile_format(&self, content: &str) -> ParserResult<LockfileData> {
+        let mut lockfile_data = LockfileData::new();
+        let mut current_section = LockfileSection::None;
+        let mut current_remote = String::new();
+        let mut specs_indent = 0;
+
+        for line in content.lines() {
+            let trimmed = line.trim();
+            
+            // Skip empty lines and comments
+            if trimmed.is_empty() || trimmed.starts_with('#') {
+                continue;
+            }
+
+            // Detect section headers
+            if let Some(section) = self.detect_section(trimmed) {
+                current_section = section;
+                continue;
+            }
+
+            match current_section {
+                LockfileSection::Gem => {
+                    if line.starts_with("  remote:") {
+                        current_remote = line.trim_start_matches("  remote:").trim().to_string();
+                    } else if line.starts_with("  specs:") {
+                        // Start of specs section
+                        continue;
+                    } else if line.starts_with("    ") {
+                        // This is a gem specification
+                        if specs_indent == 0 {
+                            specs_indent = line.len() - line.trim_start().len();
+                        }
+                        
+                        if line.len() - line.trim_start().len() == specs_indent {
+                            if let Some(spec) = self.parse_gem_spec(line.trim(), &current_remote) {
+                                lockfile_data.specs.push(spec);
+                            }
+                        }
+                    }
+                }
+                LockfileSection::Platforms => {
+                    if line.starts_with("  ") {
+                        lockfile_data.platforms.push(line.trim().to_string());
+                    }
+                }
+                LockfileSection::Dependencies => {
+                    if line.starts_with("  ") {
+                        lockfile_data.dependencies.push(line.trim().to_string());
+                    }
+                }
+                LockfileSection::None => {
+                    // Not in a recognized section, skip
+                }
+            }
+        }
+
+        Ok(lockfile_data)
+    }
+
+    fn detect_section(&self, line: &str) -> Option<LockfileSection> {
+        match line {
+            "GEM" => Some(LockfileSection::Gem),
+            "PLATFORMS" => Some(LockfileSection::Platforms),
+            "DEPENDENCIES" => Some(LockfileSection::Dependencies),
+            _ => None,
+        }
+    }
+
+    fn parse_gem_spec(&self, line: &str, remote: &str) -> Option<GemSpec> {
+        // Parse lines like: "net-hippie (0.2.7)"
+        // or: "nokogiri (1.10.10-x86_64-darwin)"
+        
+        if let Some(captures) = self.extract_name_version(line) {
+            let (name, version) = captures;
+            Some(GemSpec {
+                name,
+                version,
+                platform: "ruby".to_string(), // Default platform
+                source: remote.to_string(),
+                dependencies: Vec::new(),
+            })
+        } else {
+            warn!("Failed to parse gem spec line: {}", line);
+            None
+        }
+    }
+
+    fn extract_name_version(&self, line: &str) -> Option<(String, String)> {
+        // Handle various formats:
+        // "gem_name (version)"
+        // "gem_name (version-platform)"
+        
+        if let Some(paren_start) = line.find('(') {
+            if let Some(paren_end) = line.rfind(')') {
+                let name = line[..paren_start].trim().to_string();
+                let version_part = line[paren_start + 1..paren_end].trim();
+                
+                // Extract version, potentially removing platform suffix
+                // Only remove suffix if it looks like a platform (e.g., x86_64-darwin, java)
+                // But keep version suffixes like beta-1, rc-2, etc.
+                let version = if version_part.contains('-') {
+                    // Common platform identifiers
+                    let platform_indicators = ["x86", "darwin", "java", "mswin", "mingw"];
+                    
+                    if platform_indicators.iter().any(|&p| version_part.contains(p)) {
+                        // For platform-specific versions like "1.10.10-x86_64-darwin", take the first part
+                        version_part.split('-').next().unwrap_or(version_part).to_string()
+                    } else {
+                        // For version suffixes like "1.0.0-beta-1", keep the whole thing
+                        version_part.to_string()
+                    }
+                } else {
+                    version_part.to_string()
+                };
+                
+                return Some((name, version));
+            }
+        }
+        
+        None
+    }
+}
+
+impl Default for GemfileLockParser {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[async_trait]
+impl Parser for GemfileLockParser {
+    fn can_parse(&self, path: &Utf8Path) -> bool {
+        if let Some(filename) = path.file_name() {
+            self.matches_filename(filename)
+        } else {
+            false
+        }
+    }
+
+    async fn parse(&self, path: &Utf8Path) -> ParserResult<DependencyCollection> {
+        let content = tokio::fs::read_to_string(path).await.map_err(ParserError::IoError)?;
+        self.parse_gemfile_content(&content, path).await
+    }
+
+    fn name(&self) -> &'static str {
+        "gemfile-lock"
+    }
+
+    fn file_patterns(&self) -> Vec<&'static str> {
+        vec!["Gemfile*.lock", "gems*.lock"]
+    }
+}
+
+#[derive(Debug, PartialEq)]
+enum LockfileSection {
+    None,
+    Gem,
+    Platforms,
+    Dependencies,
+}
+
+#[derive(Debug)]
+struct LockfileData {
+    specs: Vec<GemSpec>,
+    platforms: Vec<String>,
+    dependencies: Vec<String>,
+}
+
+impl LockfileData {
+    fn new() -> Self {
+        Self {
+            specs: Vec::new(),
+            platforms: Vec::new(),
+            dependencies: Vec::new(),
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+struct GemSpec {
+    name: String,
+    version: String,
+    platform: String,
+    source: String,
+    #[allow(dead_code)]
+    dependencies: Vec<String>,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use camino::Utf8PathBuf;
+    use tempfile::NamedTempFile;
+    use std::io::Write;
+
+    #[test]
+    fn test_filename_matching() {
+        let parser = GemfileLockParser::new();
+        
+        assert!(parser.matches_filename("Gemfile.lock"));
+        assert!(parser.matches_filename("Gemfile.development.lock"));
+        assert!(parser.matches_filename("gems.lock"));
+        assert!(parser.matches_filename("gems.production.lock"));
+        
+        assert!(!parser.matches_filename("package.json"));
+        assert!(!parser.matches_filename("Gemfile"));
+        assert!(!parser.matches_filename("something.lock"));
+    }
+
+    #[test]
+    fn test_can_parse() {
+        let parser = GemfileLockParser::new();
+        
+        assert!(parser.can_parse(Utf8Path::new("/path/to/Gemfile.lock")));
+        assert!(parser.can_parse(Utf8Path::new("/path/to/gems.lock")));
+        assert!(!parser.can_parse(Utf8Path::new("/path/to/package.json")));
+    }
+
+    #[test]
+    fn test_extract_name_version() {
+        let parser = GemfileLockParser::new();
+        
+        assert_eq!(
+            parser.extract_name_version("net-hippie (0.2.7)"),
+            Some(("net-hippie".to_string(), "0.2.7".to_string()))
+        );
+        
+        assert_eq!(
+            parser.extract_name_version("nokogiri (1.10.10-x86_64-darwin)"),
+            Some(("nokogiri".to_string(), "1.10.10".to_string()))
+        );
+        
+        assert_eq!(
+            parser.extract_name_version("some-gem (1.0.0-java)"),
+            Some(("some-gem".to_string(), "1.0.0".to_string()))
+        );
+        
+        // Version with dashes that aren't platform suffixes
+        assert_eq!(
+            parser.extract_name_version("pre-release (1.0.0-beta-1)"),
+            Some(("pre-release".to_string(), "1.0.0-beta-1".to_string()))
+        );
+    }
+
+    #[tokio::test]
+    async fn test_parse_simple_gemfile_lock() {
+        let content = r#"GEM
+  remote: https://rubygems.org/
+  specs:
+    net-hippie (0.2.7)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  net-hippie
+
+BUNDLED WITH
+   1.17.3
+"#;
+
+        let parser = GemfileLockParser::new();
+        let mut temp_file = NamedTempFile::new().unwrap();
+        write!(temp_file, "{}", content).unwrap();
+        
+        let path = Utf8PathBuf::try_from(temp_file.path().to_path_buf()).unwrap();
+        let result = parser.parse_gemfile_content(content, &path).await.unwrap();
+        
+        assert_eq!(result.len(), 1);
+        let deps: Vec<_> = result.into_iter().collect();
+        
+        assert_eq!(deps[0].name, "net-hippie");
+        assert_eq!(deps[0].version, "0.2.7");
+        assert_eq!(deps[0].metadata.get("source"), Some(&"https://rubygems.org/".to_string()));
+    }
+
+    #[test]
+    fn test_bundled_with_removal() {
+        let parser = GemfileLockParser::new();
+        let content = "Some content\nBUNDLED WITH\n   1.17.3\nMore content";
+        let cleaned = parser.strip_bundled_with.replace_all(content, "");
+        assert_eq!(cleaned, "Some content\nMore content");
+    }
+
+    #[test]
+    fn test_section_detection() {
+        let parser = GemfileLockParser::new();
+        
+        assert_eq!(parser.detect_section("GEM"), Some(LockfileSection::Gem));
+        assert_eq!(parser.detect_section("PLATFORMS"), Some(LockfileSection::Platforms));
+        assert_eq!(parser.detect_section("DEPENDENCIES"), Some(LockfileSection::Dependencies));
+        assert_eq!(parser.detect_section("OTHER"), None);
+    }
+
+    #[test]
+    fn test_parser_name_and_patterns() {
+        let parser = GemfileLockParser::new();
+        assert_eq!(parser.name(), "gemfile-lock");
+        assert_eq!(parser.file_patterns(), vec!["Gemfile*.lock", "gems*.lock"]);
+    }
+}
\ No newline at end of file
src/parsers/ruby/mod.rs
@@ -0,0 +1,3 @@
+pub mod gemfile_lock;
+
+pub use gemfile_lock::*;
\ No newline at end of file
src/parsers/terraform/lock_file.rs
@@ -0,0 +1,202 @@
+use crate::core::{
+    parser::{Parser, ParserError, ParserResult},
+    Dependency, DependencyCollection,
+};
+use async_trait::async_trait;
+use camino::Utf8Path;
+use std::collections::HashMap;
+use tracing::debug;
+
+#[derive(Debug)]
+pub struct TerraformLockParser;
+
+impl TerraformLockParser {
+    pub fn new() -> Self {
+        Self
+    }
+    
+    fn matches_filename(&self, filename: &str) -> bool {
+        filename == ".terraform.lock.hcl"
+    }
+}
+
+impl Default for TerraformLockParser {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[async_trait]
+impl Parser for TerraformLockParser {
+    fn can_parse(&self, path: &Utf8Path) -> bool {
+        path.file_name()
+            .map(|name| self.matches_filename(name))
+            .unwrap_or(false)
+    }
+    
+    fn name(&self) -> &'static str {
+        "terraform"
+    }
+    
+    fn file_patterns(&self) -> Vec<&'static str> {
+        vec![".terraform.lock.hcl"]
+    }
+
+    async fn parse(&self, path: &Utf8Path) -> ParserResult<DependencyCollection> {
+        debug!("Parsing .terraform.lock.hcl at: {}", path);
+        
+        let content = tokio::fs::read_to_string(path)
+            .await
+            .map_err(|_| ParserError::FileNotFound(path.to_path_buf()))?;
+        
+        let mut dependencies = DependencyCollection::new();
+        
+        // Parse HCL content line by line to extract provider blocks
+        self.parse_hcl_content(&content, path, &mut dependencies)?;
+        
+        debug!("Found {} dependencies in .terraform.lock.hcl", dependencies.len());
+        Ok(dependencies)
+    }
+}
+
+impl TerraformLockParser {
+    fn parse_hcl_content(
+        &self,
+        content: &str,
+        path: &Utf8Path,
+        dependencies: &mut DependencyCollection,
+    ) -> ParserResult<()> {
+        let mut lines = content.lines().peekable();
+        
+        while let Some(line) = lines.next() {
+            let trimmed = line.trim();
+            
+            // Look for provider blocks
+            if trimmed.starts_with("provider ") {
+                if let Some(provider_name) = self.extract_provider_name(trimmed) {
+                    // Parse the provider block
+                    if let Some(dependency) = self.parse_provider_block(&provider_name, &mut lines, path)? {
+                        dependencies.add(dependency);
+                    }
+                }
+            }
+        }
+        
+        Ok(())
+    }
+    
+    fn extract_provider_name(&self, line: &str) -> Option<String> {
+        // Extract provider name from line like: provider "registry.terraform.io/hashicorp/aws" {
+        if let Some(start) = line.find('"') {
+            if let Some(end) = line[start + 1..].find('"') {
+                return Some(line[start + 1..start + 1 + end].to_string());
+            }
+        }
+        None
+    }
+    
+    fn parse_provider_block(
+        &self,
+        provider_name: &str,
+        lines: &mut std::iter::Peekable<std::str::Lines>,
+        path: &Utf8Path,
+    ) -> ParserResult<Option<Dependency>> {
+        let mut version = String::new();
+        let mut constraints = String::new();
+        let mut hashes = Vec::new();
+        let mut brace_count = 1; // We've already seen the opening brace
+        
+        while let Some(line) = lines.next() {
+            let trimmed = line.trim();
+            
+            // Track braces to know when the block ends
+            brace_count += trimmed.chars().filter(|&c| c == '{').count();
+            brace_count -= trimmed.chars().filter(|&c| c == '}').count();
+            
+            if brace_count == 0 {
+                break; // End of provider block
+            }
+            
+            // Parse version
+            if trimmed.starts_with("version") {
+                if let Some(extracted_version) = self.extract_quoted_value(trimmed) {
+                    version = extracted_version;
+                }
+            }
+            
+            // Parse constraints
+            if trimmed.starts_with("constraints") {
+                if let Some(extracted_constraints) = self.extract_quoted_value(trimmed) {
+                    constraints = extracted_constraints;
+                }
+            }
+            
+            // Parse hashes (multiline array)
+            if trimmed.starts_with("hashes") && trimmed.contains('[') {
+                // Start of hashes array
+                if !trimmed.ends_with(']') {
+                    // Multiline array, read until closing bracket
+                    while let Some(hash_line) = lines.next() {
+                        let hash_trimmed = hash_line.trim();
+                        if hash_trimmed.contains(']') {
+                            break;
+                        }
+                        // Extract quoted strings from hash lines
+                        if hash_trimmed.starts_with('"') && hash_trimmed.ends_with(',') {
+                            let hash_value = hash_trimmed.trim_end_matches(',').trim_matches('"');
+                            if !hash_value.is_empty() {
+                                hashes.push(hash_value.to_string());
+                            }
+                        } else if hash_trimmed.starts_with('"') && hash_trimmed.ends_with('"') {
+                            let hash_value = hash_trimmed.trim_matches('"');
+                            if !hash_value.is_empty() {
+                                hashes.push(hash_value.to_string());
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        
+        if version.is_empty() {
+            return Ok(None);
+        }
+        
+        let mut meta = HashMap::new();
+        
+        if !constraints.is_empty() {
+            meta.insert("constraints".to_string(), constraints);
+        }
+        
+        if !hashes.is_empty() {
+            meta.insert("hashes".to_string(), hashes.join(","));
+        }
+        
+        // Extract provider parts for metadata
+        let parts: Vec<&str> = provider_name.split('/').collect();
+        if parts.len() >= 3 {
+            meta.insert("registry".to_string(), parts[0].to_string());
+            meta.insert("namespace".to_string(), parts[1].to_string());
+            meta.insert("name".to_string(), parts[2].to_string());
+        }
+        
+        let mut dependency = Dependency::new(provider_name.to_string(), version);
+        dependency.location = path.to_path_buf();
+        dependency.metadata = meta;
+        
+        Ok(Some(dependency))
+    }
+    
+    fn extract_quoted_value(&self, line: &str) -> Option<String> {
+        // Extract value from lines like: version = "3.39.0"
+        if let Some(equals_pos) = line.find('=') {
+            let value_part = line[equals_pos + 1..].trim();
+            if let Some(start) = value_part.find('"') {
+                if let Some(end) = value_part[start + 1..].find('"') {
+                    return Some(value_part[start + 1..start + 1 + end].to_string());
+                }
+            }
+        }
+        None
+    }
+}
\ No newline at end of file
src/parsers/terraform/mod.rs
@@ -0,0 +1,6 @@
+pub mod lock_file;
+
+#[cfg(test)]
+mod tests;
+
+pub use lock_file::TerraformLockParser;
\ No newline at end of file
src/parsers/terraform/tests.rs
@@ -0,0 +1,230 @@
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::core::parser::Parser;
+    use crate::parsers::TerraformLockParser;
+    use camino::Utf8PathBuf;
+    use std::fs;
+    use tempfile::tempdir;
+
+    #[test]
+    fn test_terraform_lock_parser_can_parse() {
+        let parser = TerraformLockParser::new();
+        
+        assert!(parser.can_parse(Utf8PathBuf::from("/path/to/.terraform.lock.hcl").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/main.tf").as_path()));
+        assert!(!parser.can_parse(Utf8PathBuf::from("/path/to/terraform.tfstate").as_path()));
+    }
+
+    #[tokio::test]
+    async fn test_terraform_lock_parser_parse_basic() {
+        let content = r#"# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/hashicorp/aws" {
+  version     = "3.39.0"
+  constraints = "~> 3.27"
+  hashes = [
+    "h1:fjlp3Pd3QsTLghNm7TUh/KnEMM2D3tLb7jsDLs8oWUE=",
+    "zh:2014b397dd93fa55f2f2d1338c19e5b2b77b025a76a6b1fceea0b8696e984b9c",
+    "zh:23d59c68ab50148a0f5c911a801734e9934a1fccd41118a8efb5194135cbd360",
+  ]
+}
+
+provider "registry.terraform.io/hashicorp/random" {
+  version = "3.1.0"
+  hashes = [
+    "h1:rKYu5ZUbXwrLG1w81k7H3nce/Ys6yAxXhWcbtk36HjY=",
+    "zh:2bbb3339f0643b5daa07480ef4397bd23a79963cc364cdfbb4e86354cb7725bc",
+  ]
+}
+"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join(".terraform.lock.hcl");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = TerraformLockParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 2);
+        
+        // Check AWS provider
+        let aws = result.iter()
+            .find(|dep| dep.name == "registry.terraform.io/hashicorp/aws")
+            .expect("AWS provider not found");
+        assert_eq!(aws.version, "3.39.0");
+        assert_eq!(aws.metadata.get("constraints"), Some(&"~> 3.27".to_string()));
+        assert_eq!(aws.metadata.get("registry"), Some(&"registry.terraform.io".to_string()));
+        assert_eq!(aws.metadata.get("namespace"), Some(&"hashicorp".to_string()));
+        assert_eq!(aws.metadata.get("name"), Some(&"aws".to_string()));
+        assert!(aws.metadata.get("hashes").is_some());
+        
+        // Check Random provider
+        let random = result.iter()
+            .find(|dep| dep.name == "registry.terraform.io/hashicorp/random")
+            .expect("Random provider not found");
+        assert_eq!(random.version, "3.1.0");
+        assert_eq!(random.metadata.get("registry"), Some(&"registry.terraform.io".to_string()));
+        assert_eq!(random.metadata.get("namespace"), Some(&"hashicorp".to_string()));
+        assert_eq!(random.metadata.get("name"), Some(&"random".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_terraform_lock_parser_empty_file() {
+        let content = r#"# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join(".terraform.lock.hcl");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = TerraformLockParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 0);
+    }
+
+    #[tokio::test]
+    async fn test_terraform_lock_parser_single_provider() {
+        let content = r#"provider "registry.terraform.io/hashicorp/aws" {
+  version = "4.0.0"
+  constraints = ">= 3.0"
+  hashes = [
+    "h1:example1234567890abcdef",
+  ]
+}
+"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join(".terraform.lock.hcl");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = TerraformLockParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 1);
+        
+        let aws = &result.iter().next().unwrap();
+        assert_eq!(aws.name, "registry.terraform.io/hashicorp/aws");
+        assert_eq!(aws.version, "4.0.0");
+        assert_eq!(aws.metadata.get("constraints"), Some(&">= 3.0".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_terraform_lock_parser_missing_version() {
+        let content = r#"provider "registry.terraform.io/hashicorp/aws" {
+  constraints = "~> 3.27"
+  hashes = [
+    "h1:fjlp3Pd3QsTLghNm7TUh/KnEMM2D3tLb7jsDLs8oWUE=",
+  ]
+}
+
+provider "registry.terraform.io/hashicorp/random" {
+  version = "3.1.0"
+  hashes = [
+    "h1:rKYu5ZUbXwrLG1w81k7H3nce/Ys6yAxXhWcbtk36HjY=",
+  ]
+}
+"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join(".terraform.lock.hcl");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = TerraformLockParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        // Should only include random provider (AWS missing version)
+        assert_eq!(result.len(), 1);
+        
+        let random = result.iter()
+            .find(|dep| dep.name == "registry.terraform.io/hashicorp/random")
+            .expect("Random provider not found");
+        assert_eq!(random.version, "3.1.0");
+    }
+
+    #[tokio::test]
+    async fn test_terraform_lock_parser_complex_nested() {
+        let content = r#"# This file is maintained automatically by "terraform init".
+
+provider "registry.terraform.io/hashicorp/aws" {
+  version     = "4.15.1"
+  constraints = "~> 4.0"
+  hashes = [
+    "h1:1iA2SdDzmQh6UfM0/AjWW/+e4DWlOXhYFiOJd7GhKdM=",
+    "zh:1d148c5c889c636765b9e15a37f9c7e0a4b94821cb58e0b31e3e0ac0e2dfdeeb",
+    "zh:2fcdb8ae4a2267e45a5e10b5e0b0ab50f5e2f32c21622b4050c8e60ad7d45bd7",
+  ]
+}
+
+provider "registry.terraform.io/hashicorp/kubernetes" {
+  version     = "2.11.0"
+  constraints = ">= 2.0.0"
+  hashes = [
+    "h1:T65SuPpnCHSfLd3c2bsv0q9ZfjCFEH6jTUBOE1Fs7Bg=",
+    "zh:143a19dd0ea3b07fc5e3d9231f3c2d01f92894385c98a67327de74c76c715843",
+    "zh:1fc757d209e09c3cf7848e4274daa32408c07743698fbed10ee52a4a479b62b6",
+  ]
+}
+"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join(".terraform.lock.hcl");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = TerraformLockParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 2);
+        
+        // Check AWS provider
+        let aws = result.iter()
+            .find(|dep| dep.name == "registry.terraform.io/hashicorp/aws")
+            .expect("AWS provider not found");
+        assert_eq!(aws.version, "4.15.1");
+        assert_eq!(aws.metadata.get("constraints"), Some(&"~> 4.0".to_string()));
+        
+        // Check Kubernetes provider
+        let k8s = result.iter()
+            .find(|dep| dep.name == "registry.terraform.io/hashicorp/kubernetes")
+            .expect("Kubernetes provider not found");
+        assert_eq!(k8s.version, "2.11.0");
+        assert_eq!(k8s.metadata.get("constraints"), Some(&">= 2.0.0".to_string()));
+        assert_eq!(k8s.metadata.get("namespace"), Some(&"hashicorp".to_string()));
+        assert_eq!(k8s.metadata.get("name"), Some(&"kubernetes".to_string()));
+    }
+
+    #[tokio::test]
+    async fn test_terraform_lock_parser_no_constraints() {
+        let content = r#"provider "registry.terraform.io/hashicorp/local" {
+  version = "2.2.2"
+  hashes = [
+    "h1:5UYW2wJ320IggrzLt8tLD6AM9s9R5l8zjIgf3aafWAY=",
+  ]
+}
+"#;
+
+        let temp_dir = tempdir().unwrap();
+        let file_path = temp_dir.path().join(".terraform.lock.hcl");
+        fs::write(&file_path, content).unwrap();
+        
+        let parser = TerraformLockParser::new();
+        let path = Utf8PathBuf::from_path_buf(file_path).unwrap();
+        let result = parser.parse(&path).await.unwrap();
+        
+        assert_eq!(result.len(), 1);
+        
+        let local = &result.iter().next().unwrap();
+        assert_eq!(local.name, "registry.terraform.io/hashicorp/local");
+        assert_eq!(local.version, "2.2.2");
+        assert_eq!(local.metadata.get("constraints"), None); // No constraints specified
+        assert!(local.metadata.get("hashes").is_some());
+    }
+}
\ No newline at end of file
src/parsers/mod.rs
@@ -0,0 +1,17 @@
+pub mod dotnet;
+pub mod java;
+pub mod javascript;
+pub mod os;
+pub mod php;
+pub mod python;
+pub mod ruby;
+pub mod terraform;
+
+pub use dotnet::*;
+pub use java::*;
+pub use javascript::*;
+pub use os::*;
+pub use php::*;
+pub use python::*;
+pub use ruby::*;
+pub use terraform::*;
\ No newline at end of file
src/spdx/catalogue.rs
@@ -0,0 +1,291 @@
+use crate::spdx::license::License;
+use anyhow::Result;
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use tokio::fs;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct SpdxLicenseList {
+    #[serde(rename = "licenseListVersion")]
+    license_list_version: String,
+    licenses: Vec<SpdxLicenseData>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct SpdxLicenseData {
+    #[serde(rename = "licenseId")]
+    license_id: String,
+    name: String,
+    reference: String,
+    #[serde(rename = "detailsUrl")]
+    details_url: Option<String>,
+    #[serde(rename = "referenceNumber")]
+    reference_number: Option<u32>,
+    #[serde(rename = "isDeprecatedLicenseId")]
+    is_deprecated_license_id: Option<bool>,
+    #[serde(rename = "isOsiApproved")]
+    is_osi_approved: Option<bool>,
+    #[serde(rename = "seeAlso")]
+    see_also: Option<Vec<String>>,
+}
+
+#[derive(Debug, Clone)]
+pub struct Catalogue {
+    licenses: HashMap<String, License>,
+    version: String,
+}
+
+impl Catalogue {
+    pub fn new() -> Self {
+        Self {
+            licenses: HashMap::new(),
+            version: "unknown".to_string(),
+        }
+    }
+
+    pub fn from_json(json: &str) -> Result<Self> {
+        let license_list: SpdxLicenseList = serde_json::from_str(json)?;
+        
+        let mut licenses = HashMap::new();
+        for license_data in license_list.licenses {
+            if !license_data.license_id.is_empty() {
+                let license = License {
+                    id: license_data.license_id.clone(),
+                    name: license_data.name,
+                    reference: license_data.reference,
+                    url: license_data.details_url,
+                    deprecated_license_id: license_data.is_deprecated_license_id,
+                    osi_approved: license_data.is_osi_approved,
+                    see_also: license_data.see_also,
+                    reference_number: license_data.reference_number,
+                };
+                licenses.insert(license_data.license_id, license);
+            }
+        }
+        
+        Ok(Self {
+            licenses,
+            version: license_list.license_list_version,
+        })
+    }
+
+    pub async fn from_file(path: &str) -> Result<Self> {
+        let content = fs::read_to_string(path).await?;
+        Self::from_json(&content)
+    }
+
+    pub async fn from_url(url: &str) -> Result<Self> {
+        let client = reqwest::Client::new();
+        let response = client
+            .get(url)
+            .header("User-Agent", "spandx-rs/1.0.0")
+            .send()
+            .await?;
+        
+        if response.status().is_success() {
+            let content = response.text().await?;
+            Self::from_json(&content)
+        } else {
+            Err(anyhow::anyhow!("HTTP request failed: {}", response.status()))
+        }
+    }
+
+    pub async fn latest() -> Result<Self> {
+        const SPDX_LICENSE_URL: &str = "https://spdx.org/licenses/licenses.json";
+        
+        match Self::from_url(SPDX_LICENSE_URL).await {
+            Ok(catalogue) => Ok(catalogue),
+            Err(e) => {
+                eprintln!("Failed to fetch SPDX licenses from URL: {}", e);
+                Self::default_embedded()
+            }
+        }
+    }
+
+    pub fn default_embedded() -> Result<Self> {
+        let default_json = include_str!("../../resources/spdx-licenses.json");
+        Self::from_json(default_json)
+    }
+
+    pub fn get(&self, id: &str) -> Option<&License> {
+        self.licenses.get(id)
+    }
+
+    pub fn version(&self) -> &str {
+        &self.version
+    }
+
+    pub fn len(&self) -> usize {
+        self.licenses.len()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.licenses.is_empty()
+    }
+
+    pub fn iter(&self) -> impl Iterator<Item = (&String, &License)> {
+        self.licenses.iter()
+    }
+
+    pub fn licenses(&self) -> impl Iterator<Item = &License> {
+        self.licenses.values()
+    }
+
+    pub fn find<F>(&self, predicate: F) -> Option<&License>
+    where
+        F: Fn(&License) -> bool,
+    {
+        self.licenses.values().find(|license| predicate(license))
+    }
+
+    pub fn contains_key(&self, id: &str) -> bool {
+        self.licenses.contains_key(id)
+    }
+}
+
+impl std::ops::Index<&str> for Catalogue {
+    type Output = License;
+
+    fn index(&self, id: &str) -> &Self::Output {
+        &self.licenses[id]
+    }
+}
+
+impl Default for Catalogue {
+    fn default() -> Self {
+        Self::default_embedded().unwrap_or_else(|_| Self::new())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_empty_catalogue() {
+        let catalogue = Catalogue::new();
+        assert!(catalogue.is_empty());
+        assert_eq!(catalogue.len(), 0);
+        assert_eq!(catalogue.version(), "unknown");
+    }
+
+    #[test]
+    fn test_from_json() {
+        let json = r#"{
+            "licenseListVersion": "3.21",
+            "licenses": [
+                {
+                    "licenseId": "MIT",
+                    "name": "MIT License",
+                    "reference": "https://opensource.org/licenses/MIT",
+                    "isOsiApproved": true,
+                    "isDeprecatedLicenseId": false,
+                    "referenceNumber": 1
+                },
+                {
+                    "licenseId": "Apache-2.0",
+                    "name": "Apache License 2.0",
+                    "reference": "https://www.apache.org/licenses/LICENSE-2.0",
+                    "isOsiApproved": true,
+                    "isDeprecatedLicenseId": false,
+                    "referenceNumber": 2
+                }
+            ]
+        }"#;
+
+        let catalogue = Catalogue::from_json(json).unwrap();
+        assert_eq!(catalogue.len(), 2);
+        assert_eq!(catalogue.version(), "3.21");
+        
+        let mit_license = catalogue.get("MIT").unwrap();
+        assert_eq!(mit_license.id, "MIT");
+        assert_eq!(mit_license.name, "MIT License");
+        assert!(mit_license.is_osi_approved());
+        assert!(!mit_license.is_deprecated());
+        
+        let apache_license = catalogue.get("Apache-2.0").unwrap();
+        assert_eq!(apache_license.id, "Apache-2.0");
+        assert_eq!(apache_license.name, "Apache License 2.0");
+    }
+
+    #[test]
+    fn test_from_json_filters_empty_ids() {
+        let json = r#"{
+            "licenseListVersion": "3.21",
+            "licenses": [
+                {
+                    "licenseId": "",
+                    "name": "Empty License",
+                    "reference": "https://example.com"
+                },
+                {
+                    "licenseId": "MIT",
+                    "name": "MIT License",
+                    "reference": "https://opensource.org/licenses/MIT"
+                }
+            ]
+        }"#;
+
+        let catalogue = Catalogue::from_json(json).unwrap();
+        assert_eq!(catalogue.len(), 1);
+        assert!(catalogue.contains_key("MIT"));
+        assert!(!catalogue.contains_key(""));
+    }
+
+    #[test]
+    fn test_find_predicate() {
+        let json = r#"{
+            "licenseListVersion": "3.21",
+            "licenses": [
+                {
+                    "licenseId": "MIT",
+                    "name": "MIT License",
+                    "reference": "https://opensource.org/licenses/MIT",
+                    "isOsiApproved": true
+                },
+                {
+                    "licenseId": "Proprietary",
+                    "name": "Proprietary License",
+                    "reference": "https://example.com",
+                    "isOsiApproved": false
+                }
+            ]
+        }"#;
+
+        let catalogue = Catalogue::from_json(json).unwrap();
+        
+        let osi_license = catalogue.find(|license| license.is_osi_approved());
+        assert!(osi_license.is_some());
+        assert_eq!(osi_license.unwrap().id, "MIT");
+        
+        let non_osi_license = catalogue.find(|license| !license.is_osi_approved());
+        assert!(non_osi_license.is_some());
+        assert_eq!(non_osi_license.unwrap().id, "Proprietary");
+    }
+
+    #[test]
+    fn test_iteration() {
+        let json = r#"{
+            "licenseListVersion": "3.21",
+            "licenses": [
+                {
+                    "licenseId": "MIT",
+                    "name": "MIT License",
+                    "reference": "https://opensource.org/licenses/MIT"
+                },
+                {
+                    "licenseId": "Apache-2.0",
+                    "name": "Apache License 2.0",
+                    "reference": "https://www.apache.org/licenses/LICENSE-2.0"
+                }
+            ]
+        }"#;
+
+        let catalogue = Catalogue::from_json(json).unwrap();
+        
+        let license_ids: Vec<String> = catalogue.licenses().map(|l| l.id.clone()).collect();
+        assert!(license_ids.contains(&"MIT".to_string()));
+        assert!(license_ids.contains(&"Apache-2.0".to_string()));
+        assert_eq!(license_ids.len(), 2);
+    }
+}
\ No newline at end of file
src/spdx/expression.pest
@@ -0,0 +1,34 @@
+expression = { SOI ~ ws? ~ or_expression ~ ws? ~ EOI }
+
+or_expression = { and_expression ~ (ws+ ~ or_op ~ ws+ ~ and_expression)* }
+
+and_expression = { with_expression ~ (ws+ ~ and_op ~ ws+ ~ with_expression)* }
+
+with_expression = { primary ~ (ws+ ~ with_op ~ ws+ ~ exception_id)? }
+
+primary = { 
+    parenthesized_expression |
+    license_ref |
+    license_id
+}
+
+parenthesized_expression = { "(" ~ ws? ~ or_expression ~ ws? ~ ")" }
+
+or_op = { ^"OR" }
+and_op = { ^"AND" }
+with_op = { ^"WITH" }
+
+license_id = { 
+    (ASCII_ALPHANUMERIC | "-" | "." | "+" | "_")+
+}
+
+license_ref = { 
+    ("DocumentRef-" ~ license_id ~ ":" ~ "LicenseRef-" ~ license_id) |
+    ("LicenseRef-" ~ license_id)
+}
+
+exception_id = { 
+    (ASCII_ALPHANUMERIC | "-" | "." | "+" | "_")+ 
+}
+
+ws = _{ " " | "\t" | "\n" | "\r" }
\ No newline at end of file
src/spdx/expression.rs
@@ -0,0 +1,263 @@
+use crate::spdx::license::LicenseTree;
+use pest::Parser;
+use pest_derive::Parser;
+
+#[derive(Parser)]
+#[grammar = "spdx/expression.pest"]
+pub struct ExpressionParser;
+
+pub type Expression = LicenseTree;
+
+impl ExpressionParser {
+    pub fn new() -> Self {
+        Self
+    }
+
+    pub fn parse(&self, input: &str) -> Result<LicenseTree, String> {
+        let pairs = <Self as Parser<Rule>>::parse(Rule::expression, input)
+            .map_err(|e| format!("Parse error: {}", e))?;
+        
+        for pair in pairs {
+            return self.parse_expression(pair);
+        }
+        
+        Err("No valid expression found".to_string())
+    }
+
+    fn parse_expression(&self, pair: pest::iterators::Pair<Rule>) -> Result<LicenseTree, String> {
+        match pair.as_rule() {
+            Rule::expression => {
+                let mut inner = pair.into_inner();
+                if let Some(expr) = inner.next() {
+                    self.parse_expression(expr)
+                } else {
+                    Err("Empty expression".to_string())
+                }
+            }
+            Rule::or_expression => {
+                let mut inner = pair.into_inner();
+                let mut left = self.parse_expression(inner.next().ok_or("Missing left operand")?)?;
+                
+                while let Some(op) = inner.next() {
+                    if op.as_rule() == Rule::or_op {
+                        let right = self.parse_expression(inner.next().ok_or("Missing right operand")?)?;
+                        left = LicenseTree::Binary {
+                            left: Box::new(left),
+                            op: "OR".to_string(),
+                            right: Box::new(right),
+                        };
+                    }
+                }
+                
+                Ok(left)
+            }
+            Rule::and_expression => {
+                let mut inner = pair.into_inner();
+                let mut left = self.parse_expression(inner.next().ok_or("Missing left operand")?)?;
+                
+                while let Some(op) = inner.next() {
+                    if op.as_rule() == Rule::and_op {
+                        let right = self.parse_expression(inner.next().ok_or("Missing right operand")?)?;
+                        left = LicenseTree::Binary {
+                            left: Box::new(left),
+                            op: "AND".to_string(),
+                            right: Box::new(right),
+                        };
+                    }
+                }
+                
+                Ok(left)
+            }
+            Rule::with_expression => {
+                let mut inner = pair.into_inner();
+                let license = self.parse_expression(inner.next().ok_or("Missing license in WITH expression")?)?;
+                
+                if let Some(with_op) = inner.next() {
+                    if with_op.as_rule() == Rule::with_op {
+                        let exception = inner.next().ok_or("Missing exception in WITH expression")?;
+                        Ok(LicenseTree::With {
+                            license: Box::new(license),
+                            exception: exception.as_str().to_string(),
+                        })
+                    } else {
+                        Ok(license)
+                    }
+                } else {
+                    Ok(license)
+                }
+            }
+            Rule::primary => {
+                let mut inner = pair.into_inner();
+                if let Some(expr) = inner.next() {
+                    self.parse_expression(expr)
+                } else {
+                    Err("Empty primary expression".to_string())
+                }
+            }
+            Rule::parenthesized_expression => {
+                let mut inner = pair.into_inner();
+                if let Some(expr) = inner.next() {
+                    Ok(LicenseTree::Parenthesized(Box::new(self.parse_expression(expr)?)))
+                } else {
+                    Err("Empty parenthesized expression".to_string())
+                }
+            }
+            Rule::license_id => {
+                Ok(LicenseTree::License(pair.as_str().to_string()))
+            }
+            Rule::license_ref => {
+                Ok(LicenseTree::License(pair.as_str().to_string()))
+            }
+            Rule::exception_id => {
+                Ok(LicenseTree::License(pair.as_str().to_string()))
+            }
+            _ => Err(format!("Unexpected rule: {:?}", pair.as_rule())),
+        }
+    }
+}
+
+impl Default for ExpressionParser {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_simple_license() {
+        let parser = ExpressionParser::new();
+        let result = parser.parse("MIT").unwrap();
+        
+        match result {
+            LicenseTree::License(id) => assert_eq!(id, "MIT"),
+            _ => panic!("Expected simple license"),
+        }
+    }
+
+    #[test]
+    fn test_binary_and_expression() {
+        let parser = ExpressionParser::new();
+        let result = parser.parse("MIT AND Apache-2.0");
+        
+        match result {
+            Ok(tree) => {
+                // println!("Parsed tree: {:?}", tree);
+                match tree {
+                    LicenseTree::Binary { left, op, right } => {
+                        assert_eq!(op, "AND");
+                        match (left.as_ref(), right.as_ref()) {
+                            (LicenseTree::License(l), LicenseTree::License(r)) => {
+                                assert_eq!(l, "MIT");
+                                assert_eq!(r, "Apache-2.0");
+                            }
+                            _ => panic!("Expected license operands, got: {:?} and {:?}", left, right),
+                        }
+                    }
+                    _ => panic!("Expected binary expression, got: {:?}", tree),
+                }
+            }
+            Err(e) => panic!("Parse error: {}", e),
+        }
+    }
+
+    #[test]
+    fn test_binary_or_expression() {
+        let parser = ExpressionParser::new();
+        let result = parser.parse("MIT OR Apache-2.0").unwrap();
+        
+        match result {
+            LicenseTree::Binary { left, op, right } => {
+                assert_eq!(op, "OR");
+                match (left.as_ref(), right.as_ref()) {
+                    (LicenseTree::License(l), LicenseTree::License(r)) => {
+                        assert_eq!(l, "MIT");
+                        assert_eq!(r, "Apache-2.0");
+                    }
+                    _ => panic!("Expected license operands"),
+                }
+            }
+            _ => panic!("Expected binary expression"),
+        }
+    }
+
+    #[test]
+    fn test_with_expression() {
+        let parser = ExpressionParser::new();
+        let result = parser.parse("GPL-2.0 WITH Classpath-exception-2.0").unwrap();
+        
+        match result {
+            LicenseTree::With { license, exception } => {
+                assert_eq!(exception, "Classpath-exception-2.0");
+                match license.as_ref() {
+                    LicenseTree::License(id) => assert_eq!(id, "GPL-2.0"),
+                    _ => panic!("Expected license in WITH expression"),
+                }
+            }
+            _ => panic!("Expected WITH expression"),
+        }
+    }
+
+    #[test]
+    fn test_parenthesized_expression() {
+        let parser = ExpressionParser::new();
+        let result = parser.parse("(MIT OR Apache-2.0)").unwrap();
+        
+        match result {
+            LicenseTree::Parenthesized(inner) => {
+                match inner.as_ref() {
+                    LicenseTree::Binary { op, .. } => assert_eq!(op, "OR"),
+                    _ => panic!("Expected binary expression in parentheses"),
+                }
+            }
+            _ => panic!("Expected parenthesized expression"),
+        }
+    }
+
+    #[test]
+    fn test_complex_expression() {
+        let parser = ExpressionParser::new();
+        let result = parser.parse("MIT AND (Apache-2.0 OR GPL-3.0)").unwrap();
+        
+        match result {
+            LicenseTree::Binary { left, op, right } => {
+                assert_eq!(op, "AND");
+                match (left.as_ref(), right.as_ref()) {
+                    (LicenseTree::License(l), LicenseTree::Parenthesized(inner)) => {
+                        assert_eq!(l, "MIT");
+                        match inner.as_ref() {
+                            LicenseTree::Binary { op, .. } => assert_eq!(op, "OR"),
+                            _ => panic!("Expected OR in parentheses"),
+                        }
+                    }
+                    _ => panic!("Expected license and parenthesized expression"),
+                }
+            }
+            _ => panic!("Expected complex binary expression"),
+        }
+    }
+
+    #[test]
+    fn test_license_ref() {
+        let parser = ExpressionParser::new();
+        let result = parser.parse("LicenseRef-Custom").unwrap();
+        
+        match result {
+            LicenseTree::License(id) => assert_eq!(id, "LicenseRef-Custom"),
+            _ => panic!("Expected license reference"),
+        }
+    }
+
+    #[test]
+    fn test_case_insensitive_operators() {
+        let parser = ExpressionParser::new();
+        let result = parser.parse("MIT and Apache-2.0").unwrap();
+        
+        match result {
+            LicenseTree::Binary { op, .. } => assert_eq!(op, "AND"),
+            _ => panic!("Expected binary expression with normalized operator"),
+        }
+    }
+}
\ No newline at end of file
src/spdx/license.rs
@@ -0,0 +1,214 @@
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub struct License {
+    pub id: String,
+    pub name: String,
+    pub reference: String,
+    pub url: Option<String>,
+    pub deprecated_license_id: Option<bool>,
+    pub osi_approved: Option<bool>,
+    pub see_also: Option<Vec<String>>,
+    pub reference_number: Option<u32>,
+}
+
+impl License {
+    pub fn new(id: String, name: String, reference: String) -> Self {
+        Self {
+            id,
+            name,
+            reference,
+            url: None,
+            deprecated_license_id: None,
+            osi_approved: None,
+            see_also: None,
+            reference_number: None,
+        }
+    }
+
+    pub fn unknown(text: &str) -> Self {
+        Self::new(
+            "Nonstandard".to_string(),
+            text.to_string(),
+            "Nonstandard".to_string(),
+        )
+    }
+
+    pub fn is_deprecated(&self) -> bool {
+        self.deprecated_license_id.unwrap_or(false)
+    }
+
+    pub fn is_osi_approved(&self) -> bool {
+        self.osi_approved.unwrap_or(false)
+    }
+}
+
+impl std::cmp::Ord for License {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.id.cmp(&other.id)
+    }
+}
+
+impl std::cmp::PartialOrd for License {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl std::cmp::Eq for License {}
+
+impl std::fmt::Display for License {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.id)
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum LicenseTree {
+    License(String),
+    Binary {
+        left: Box<LicenseTree>,
+        op: String,
+        right: Box<LicenseTree>,
+    },
+    With {
+        license: Box<LicenseTree>,
+        exception: String,
+    },
+    Parenthesized(Box<LicenseTree>),
+}
+
+#[derive(Debug, Clone)]
+pub struct CompositeLicense {
+    tree: LicenseTree,
+    catalogue: HashMap<String, License>,
+}
+
+impl CompositeLicense {
+    pub fn from_expression(
+        expression: &str,
+        catalogue: &HashMap<String, License>,
+    ) -> Result<Self, String> {
+        use crate::spdx::expression::ExpressionParser;
+        
+        let parser = ExpressionParser::new();
+        let tree = parser.parse(expression)?;
+        
+        Ok(Self {
+            tree,
+            catalogue: catalogue.clone(),
+        })
+    }
+
+    pub fn id(&self) -> String {
+        self.tree_to_string(&self.tree)
+    }
+
+    pub fn name(&self) -> String {
+        self.tree_to_name(&self.tree)
+    }
+
+    fn tree_to_string(&self, tree: &LicenseTree) -> String {
+        match tree {
+            LicenseTree::License(id) => id.clone(),
+            LicenseTree::Binary { left, op, right } => {
+                format!(
+                    "{} {} {}",
+                    self.tree_to_string(left),
+                    op,
+                    self.tree_to_string(right)
+                )
+            }
+            LicenseTree::With { license, exception } => {
+                format!("{} WITH {}", self.tree_to_string(license), exception)
+            }
+            LicenseTree::Parenthesized(inner) => {
+                format!("({})", self.tree_to_string(inner))
+            }
+        }
+    }
+
+    fn tree_to_name(&self, tree: &LicenseTree) -> String {
+        match tree {
+            LicenseTree::License(id) => {
+                if let Some(license) = self.catalogue.get(id) {
+                    license.name.clone()
+                } else {
+                    id.clone()
+                }
+            }
+            LicenseTree::Binary { left, op, right } => {
+                format!(
+                    "{} {} {}",
+                    self.tree_to_name(left),
+                    op,
+                    self.tree_to_name(right)
+                )
+            }
+            LicenseTree::With { license, exception } => {
+                format!("{} WITH {}", self.tree_to_name(license), exception)
+            }
+            LicenseTree::Parenthesized(inner) => {
+                format!("({})", self.tree_to_name(inner))
+            }
+        }
+    }
+}
+
+impl std::fmt::Display for CompositeLicense {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.id())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_license_creation() {
+        let license = License::new(
+            "MIT".to_string(),
+            "MIT License".to_string(),
+            "https://opensource.org/licenses/MIT".to_string(),
+        );
+        assert_eq!(license.id, "MIT");
+        assert_eq!(license.name, "MIT License");
+        assert!(!license.is_deprecated());
+        assert!(!license.is_osi_approved());
+    }
+
+    #[test]
+    fn test_license_unknown() {
+        let license = License::unknown("Custom License");
+        assert_eq!(license.id, "Nonstandard");
+        assert_eq!(license.name, "Custom License");
+        assert_eq!(license.reference, "Nonstandard");
+    }
+
+    #[test]
+    fn test_license_display() {
+        let license = License::new(
+            "Apache-2.0".to_string(),
+            "Apache License 2.0".to_string(),
+            "https://www.apache.org/licenses/LICENSE-2.0".to_string(),
+        );
+        assert_eq!(format!("{}", license), "Apache-2.0");
+    }
+
+    #[test]
+    fn test_license_ordering() {
+        let mut licenses = vec![
+            License::new("MIT".to_string(), "MIT".to_string(), "".to_string()),
+            License::new("Apache-2.0".to_string(), "Apache".to_string(), "".to_string()),
+            License::new("GPL-3.0".to_string(), "GPL".to_string(), "".to_string()),
+        ];
+        
+        licenses.sort();
+        
+        assert_eq!(licenses[0].id, "Apache-2.0");
+        assert_eq!(licenses[1].id, "GPL-3.0");
+        assert_eq!(licenses[2].id, "MIT");
+    }
+}
\ No newline at end of file
src/spdx/license_expression.pest
@@ -0,0 +1,34 @@
+// SPDX License Expression Grammar
+// Based on SPDX specification: https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/
+
+WHITESPACE = _{ " " | "\t" | "\n" | "\r" }
+
+license_expression = { SOI ~ or_expression ~ EOI }
+
+or_expression = { and_expression ~ (or_operator ~ and_expression)* }
+and_expression = { with_expression ~ (and_operator ~ with_expression)* }
+with_expression = { simple_expression ~ (with_operator ~ exception_expression)? }
+
+simple_expression = {
+    license_id |
+    license_ref |
+    "(" ~ or_expression ~ ")"
+}
+
+license_id = @{ 
+    (ASCII_ALPHANUMERIC | "-" | "." | "+" | "_")+ 
+}
+
+license_ref = @{ 
+    ("LicenseRef-" | "DocumentRef-" ~ document_ref_id ~ ":LicenseRef-") ~ license_ref_id
+}
+
+document_ref_id = @{ (ASCII_ALPHANUMERIC | "-" | "." | "+" | "_")+ }
+license_ref_id = @{ (ASCII_ALPHANUMERIC | "-" | "." | "+" | "_")+ }
+
+exception_expression = { exception_id }
+exception_id = @{ (ASCII_ALPHANUMERIC | "-" | "." | "+" | "_")+ }
+
+or_operator = { "OR" }
+and_operator = { "AND" }
+with_operator = { "WITH" }
\ No newline at end of file
src/spdx/mod.rs
@@ -0,0 +1,7 @@
+pub mod catalogue;
+pub mod expression;
+pub mod license;
+
+pub use catalogue::Catalogue;
+pub use expression::{Expression, ExpressionParser};
+pub use license::{CompositeLicense, License, LicenseTree};
\ No newline at end of file
src/error.rs
@@ -0,0 +1,569 @@
+use std::fmt;
+use thiserror::Error;
+
+/// Comprehensive error types for Spandx
+#[derive(Error, Debug)]
+pub enum SpandxError {
+    // Core dependency and license errors
+    #[error("Dependency parsing failed: {message}")]
+    DependencyParseError { message: String, source: Option<Box<dyn std::error::Error + Send + Sync>> },
+    
+    #[error("License detection failed for {package}@{version}: {reason}")]
+    LicenseDetectionError { package: String, version: String, reason: String },
+    
+    #[error("Invalid license expression: {expression}")]
+    InvalidLicenseExpression { expression: String, source: Option<Box<dyn std::error::Error + Send + Sync>> },
+    
+    // File system and I/O errors
+    #[error("File operation failed: {operation} on {path}")]
+    FileSystemError { operation: String, path: String, source: std::io::Error },
+    
+    #[error("File not found: {path}")]
+    FileNotFound { path: String },
+    
+    #[error("Directory not found: {path}")]
+    DirectoryNotFound { path: String },
+    
+    #[error("Permission denied: {path}")]
+    PermissionDenied { path: String },
+    
+    // Network and HTTP errors
+    #[error("Network request failed: {method} {url}")]
+    NetworkError { method: String, url: String, source: reqwest::Error },
+    
+    #[error("HTTP error {status}: {url}")]
+    HttpError { status: u16, url: String, message: String },
+    
+    #[error("Request timeout: {url} (after {timeout_ms}ms)")]
+    RequestTimeout { url: String, timeout_ms: u64 },
+    
+    #[error("Circuit breaker open for {service}: {reason}")]
+    CircuitBreakerOpen { service: String, reason: String },
+    
+    // Git operations errors
+    #[error("Git operation failed: {operation} on {repository}")]
+    GitError { operation: String, repository: String, source: git2::Error },
+    
+    #[error("Git repository not found: {path}")]
+    GitRepositoryNotFound { path: String },
+    
+    #[error("Git authentication failed: {repository}")]
+    GitAuthenticationError { repository: String },
+    
+    #[error("Git merge conflict in {repository}: {files:?}")]
+    GitMergeConflict { repository: String, files: Vec<String> },
+    
+    // Cache errors
+    #[error("Cache operation failed: {operation}")]
+    CacheError { operation: String, source: Option<Box<dyn std::error::Error + Send + Sync>> },
+    
+    #[error("Cache corruption detected: {details}")]
+    CacheCorruption { details: String },
+    
+    #[error("Cache index rebuild failed for {package_manager}: {reason}")]
+    CacheIndexError { package_manager: String, reason: String },
+    
+    #[error("Cache capacity exceeded: {current_size} > {max_size}")]
+    CacheCapacityError { current_size: usize, max_size: usize },
+    
+    // Parser errors
+    #[error("Failed to parse {file_type} file: {file_path}")]
+    ParseError { file_type: String, file_path: String, source: Box<dyn std::error::Error + Send + Sync> },
+    
+    #[error("Invalid {format} format in {file_path}: {reason}")]
+    InvalidFormatError { format: String, file_path: String, reason: String },
+    
+    #[error("Missing required field '{field}' in {file_path}")]
+    MissingFieldError { field: String, file_path: String },
+    
+    #[error("Unsupported file type: {file_type} (supported: {supported:?})")]
+    UnsupportedFileType { file_type: String, supported: Vec<String> },
+    
+    // Configuration errors
+    #[error("Configuration error: {message}")]
+    ConfigError { message: String, source: Option<Box<dyn std::error::Error + Send + Sync>> },
+    
+    #[error("Invalid configuration value for '{key}': {value}")]
+    InvalidConfigValue { key: String, value: String },
+    
+    #[error("Missing required configuration: {key}")]
+    MissingConfig { key: String },
+    
+    // SPDX and catalog errors
+    #[error("SPDX catalog error: {message}")]
+    SpdxError { message: String, source: Option<Box<dyn std::error::Error + Send + Sync>> },
+    
+    #[error("SPDX license not found: {license_id}")]
+    SpdxLicenseNotFound { license_id: String },
+    
+    #[error("SPDX expression parsing failed: {expression}")]
+    SpdxExpressionError { expression: String, source: Option<Box<dyn std::error::Error + Send + Sync>> },
+    
+    // CLI and user interface errors
+    #[error("Invalid command arguments: {message}")]
+    InvalidArguments { message: String },
+    
+    #[error("Operation cancelled by user")]
+    UserCancelled,
+    
+    #[error("CLI error: {message}")]
+    CliError { message: String },
+    
+    // Gateway and registry errors
+    #[error("Package registry error for {registry}: {message}")]
+    RegistryError { registry: String, message: String, source: Option<Box<dyn std::error::Error + Send + Sync>> },
+    
+    #[error("Package not found: {package}@{version} in {registry}")]
+    PackageNotFound { package: String, version: String, registry: String },
+    
+    #[error("Registry authentication failed: {registry}")]
+    RegistryAuthError { registry: String },
+    
+    #[error("Rate limit exceeded for {registry}: retry after {retry_after_ms}ms")]
+    RateLimitExceeded { registry: String, retry_after_ms: u64 },
+    
+    // Validation and data errors
+    #[error("Validation failed: {field} - {reason}")]
+    ValidationError { field: String, reason: String },
+    
+    #[error("Data corruption detected: {details}")]
+    DataCorruption { details: String },
+    
+    #[error("Serialization error: {message}")]
+    SerializationError { message: String, source: Box<dyn std::error::Error + Send + Sync> },
+    
+    // Internal errors
+    #[error("Internal error: {message}")]
+    InternalError { message: String },
+    
+    #[error("Feature not implemented: {feature}")]
+    NotImplemented { feature: String },
+    
+    #[error("Resource exhausted: {resource}")]
+    ResourceExhausted { resource: String },
+    
+    // Compatibility and migration errors
+    #[error("Version compatibility error: requires {required}, found {found}")]
+    VersionCompatibilityError { required: String, found: String },
+    
+    #[error("Migration failed from {from_version} to {to_version}: {reason}")]
+    MigrationError { from_version: String, to_version: String, reason: String },
+}
+
+impl SpandxError {
+    /// Create a dependency parse error with context
+    pub fn dependency_parse(message: impl Into<String>) -> Self {
+        Self::DependencyParseError { 
+            message: message.into(), 
+            source: None 
+        }
+    }
+    
+    /// Create a dependency parse error with source
+    pub fn dependency_parse_with_source(message: impl Into<String>, source: impl std::error::Error + Send + Sync + 'static) -> Self {
+        Self::DependencyParseError { 
+            message: message.into(), 
+            source: Some(Box::new(source)) 
+        }
+    }
+    
+    /// Create a license detection error
+    pub fn license_detection(package: impl Into<String>, version: impl Into<String>, reason: impl Into<String>) -> Self {
+        Self::LicenseDetectionError {
+            package: package.into(),
+            version: version.into(),
+            reason: reason.into(),
+        }
+    }
+    
+    /// Create a file system error
+    pub fn file_system(operation: impl Into<String>, path: impl Into<String>, source: std::io::Error) -> Self {
+        Self::FileSystemError {
+            operation: operation.into(),
+            path: path.into(),
+            source,
+        }
+    }
+    
+    /// Create a network error
+    pub fn network(method: impl Into<String>, url: impl Into<String>, source: reqwest::Error) -> Self {
+        Self::NetworkError {
+            method: method.into(),
+            url: url.into(),
+            source,
+        }
+    }
+    
+    /// Create a git error
+    pub fn git(operation: impl Into<String>, repository: impl Into<String>, source: git2::Error) -> Self {
+        Self::GitError {
+            operation: operation.into(),
+            repository: repository.into(),
+            source,
+        }
+    }
+    
+    /// Create a cache error
+    pub fn cache(operation: impl Into<String>) -> Self {
+        Self::CacheError {
+            operation: operation.into(),
+            source: None,
+        }
+    }
+    
+    /// Create a cache error with source
+    pub fn cache_with_source(operation: impl Into<String>, source: impl std::error::Error + Send + Sync + 'static) -> Self {
+        Self::CacheError {
+            operation: operation.into(),
+            source: Some(Box::new(source)),
+        }
+    }
+    
+    /// Create a parse error
+    pub fn parse(file_type: impl Into<String>, file_path: impl Into<String>, source: impl std::error::Error + Send + Sync + 'static) -> Self {
+        Self::ParseError {
+            file_type: file_type.into(),
+            file_path: file_path.into(),
+            source: Box::new(source),
+        }
+    }
+    
+    /// Create a registry error
+    pub fn registry(registry: impl Into<String>, message: impl Into<String>) -> Self {
+        Self::RegistryError {
+            registry: registry.into(),
+            message: message.into(),
+            source: None,
+        }
+    }
+    
+    /// Create a validation error
+    pub fn validation(field: impl Into<String>, reason: impl Into<String>) -> Self {
+        Self::ValidationError {
+            field: field.into(),
+            reason: reason.into(),
+        }
+    }
+    
+    /// Check if error is retriable
+    pub fn is_retriable(&self) -> bool {
+        match self {
+            Self::NetworkError { .. } => true,
+            Self::RequestTimeout { .. } => true,
+            Self::HttpError { status, .. } => *status >= 500 || *status == 429,
+            Self::GitError { .. } => true,
+            Self::CacheError { .. } => false, // Cache errors usually indicate corruption
+            Self::RateLimitExceeded { .. } => true,
+            Self::CircuitBreakerOpen { .. } => false, // Circuit breaker prevents retries
+            _ => false,
+        }
+    }
+    
+    /// Get retry delay in milliseconds
+    pub fn retry_delay_ms(&self) -> Option<u64> {
+        match self {
+            Self::NetworkError { .. } => Some(1000), // 1 second
+            Self::RequestTimeout { .. } => Some(5000), // 5 seconds
+            Self::HttpError { status, .. } => {
+                match *status {
+                    429 => Some(60000), // 1 minute for rate limiting
+                    502 | 503 | 504 => Some(2000), // 2 seconds for server errors
+                    _ => None,
+                }
+            }
+            Self::RateLimitExceeded { retry_after_ms, .. } => Some(*retry_after_ms),
+            _ => None,
+        }
+    }
+    
+    /// Get user-friendly error message
+    pub fn user_message(&self) -> String {
+        match self {
+            Self::FileNotFound { path } => format!("File not found: {}", path),
+            Self::DirectoryNotFound { path } => format!("Directory not found: {}", path),
+            Self::PermissionDenied { path } => format!("Permission denied accessing: {}", path),
+            Self::NetworkError { url, .. } => format!("Network error accessing: {}", url),
+            Self::PackageNotFound { package, version, registry } => {
+                format!("Package {}@{} not found in {}", package, version, registry)
+            }
+            Self::InvalidArguments { message } => message.clone(),
+            Self::UserCancelled => "Operation cancelled".to_string(),
+            Self::ConfigError { message, .. } => format!("Configuration error: {}", message),
+            Self::NotImplemented { feature } => format!("Feature not yet implemented: {}", feature),
+            _ => self.to_string(),
+        }
+    }
+    
+    /// Get error category for metrics and logging
+    pub fn category(&self) -> ErrorCategory {
+        match self {
+            Self::DependencyParseError { .. } | Self::LicenseDetectionError { .. } | Self::InvalidLicenseExpression { .. } => ErrorCategory::Parse,
+            Self::FileSystemError { .. } | Self::FileNotFound { .. } | Self::DirectoryNotFound { .. } | Self::PermissionDenied { .. } => ErrorCategory::FileSystem,
+            Self::NetworkError { .. } | Self::HttpError { .. } | Self::RequestTimeout { .. } | Self::CircuitBreakerOpen { .. } => ErrorCategory::Network,
+            Self::GitError { .. } | Self::GitRepositoryNotFound { .. } | Self::GitAuthenticationError { .. } | Self::GitMergeConflict { .. } => ErrorCategory::Git,
+            Self::CacheError { .. } | Self::CacheCorruption { .. } | Self::CacheIndexError { .. } | Self::CacheCapacityError { .. } => ErrorCategory::Cache,
+            Self::ParseError { .. } | Self::InvalidFormatError { .. } | Self::MissingFieldError { .. } | Self::UnsupportedFileType { .. } => ErrorCategory::Parse,
+            Self::ConfigError { .. } | Self::InvalidConfigValue { .. } | Self::MissingConfig { .. } => ErrorCategory::Config,
+            Self::SpdxError { .. } | Self::SpdxLicenseNotFound { .. } | Self::SpdxExpressionError { .. } => ErrorCategory::Spdx,
+            Self::InvalidArguments { .. } | Self::UserCancelled | Self::CliError { .. } => ErrorCategory::Cli,
+            Self::RegistryError { .. } | Self::PackageNotFound { .. } | Self::RegistryAuthError { .. } | Self::RateLimitExceeded { .. } => ErrorCategory::Registry,
+            Self::ValidationError { .. } | Self::DataCorruption { .. } | Self::SerializationError { .. } => ErrorCategory::Validation,
+            Self::InternalError { .. } | Self::NotImplemented { .. } | Self::ResourceExhausted { .. } => ErrorCategory::Internal,
+            Self::VersionCompatibilityError { .. } | Self::MigrationError { .. } => ErrorCategory::Compatibility,
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum ErrorCategory {
+    Parse,
+    FileSystem,
+    Network,
+    Git,
+    Cache,
+    Config,
+    Spdx,
+    Cli,
+    Registry,
+    Validation,
+    Internal,
+    Compatibility,
+}
+
+impl fmt::Display for ErrorCategory {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Parse => write!(f, "parse"),
+            Self::FileSystem => write!(f, "filesystem"),
+            Self::Network => write!(f, "network"),
+            Self::Git => write!(f, "git"),
+            Self::Cache => write!(f, "cache"),
+            Self::Config => write!(f, "config"),
+            Self::Spdx => write!(f, "spdx"),
+            Self::Cli => write!(f, "cli"),
+            Self::Registry => write!(f, "registry"),
+            Self::Validation => write!(f, "validation"),
+            Self::Internal => write!(f, "internal"),
+            Self::Compatibility => write!(f, "compatibility"),
+        }
+    }
+}
+
+/// Result type for Spandx operations
+pub type SpandxResult<T> = Result<T, SpandxError>;
+
+/// Convert common errors to SpandxError
+impl From<std::io::Error> for SpandxError {
+    fn from(err: std::io::Error) -> Self {
+        match err.kind() {
+            std::io::ErrorKind::NotFound => Self::FileNotFound { 
+                path: err.to_string() 
+            },
+            std::io::ErrorKind::PermissionDenied => Self::PermissionDenied { 
+                path: err.to_string() 
+            },
+            _ => Self::FileSystemError {
+                operation: "unknown".to_string(),
+                path: "unknown".to_string(),
+                source: err,
+            }
+        }
+    }
+}
+
+impl From<reqwest::Error> for SpandxError {
+    fn from(err: reqwest::Error) -> Self {
+        let url = err.url().map(|u| u.to_string()).unwrap_or_else(|| "unknown".to_string());
+        
+        if err.is_timeout() {
+            Self::RequestTimeout { url, timeout_ms: 30000 } // Default timeout
+        } else if err.is_status() {
+            let status = err.status().map(|s| s.as_u16()).unwrap_or(0);
+            Self::HttpError { 
+                status, 
+                url, 
+                message: err.to_string() 
+            }
+        } else {
+            Self::NetworkError {
+                method: "unknown".to_string(),
+                url,
+                source: err,
+            }
+        }
+    }
+}
+
+impl From<git2::Error> for SpandxError {
+    fn from(err: git2::Error) -> Self {
+        Self::GitError {
+            operation: "unknown".to_string(),
+            repository: "unknown".to_string(),
+            source: err,
+        }
+    }
+}
+
+impl From<serde_json::Error> for SpandxError {
+    fn from(err: serde_json::Error) -> Self {
+        Self::SerializationError {
+            message: "JSON serialization failed".to_string(),
+            source: Box::new(err),
+        }
+    }
+}
+
+impl From<serde_yaml::Error> for SpandxError {
+    fn from(err: serde_yaml::Error) -> Self {
+        Self::SerializationError {
+            message: "YAML serialization failed".to_string(),
+            source: Box::new(err),
+        }
+    }
+}
+
+impl From<toml::de::Error> for SpandxError {
+    fn from(err: toml::de::Error) -> Self {
+        Self::SerializationError {
+            message: "TOML deserialization failed".to_string(),
+            source: Box::new(err),
+        }
+    }
+}
+
+impl From<csv::Error> for SpandxError {
+    fn from(err: csv::Error) -> Self {
+        Self::SerializationError {
+            message: "CSV parsing failed".to_string(),
+            source: Box::new(err),
+        }
+    }
+}
+
+impl From<anyhow::Error> for SpandxError {
+    fn from(err: anyhow::Error) -> Self {
+        Self::InternalError {
+            message: err.to_string(),
+        }
+    }
+}
+
+impl From<std::string::FromUtf8Error> for SpandxError {
+    fn from(err: std::string::FromUtf8Error) -> Self {
+        Self::SerializationError {
+            message: "UTF-8 conversion failed".to_string(),
+            source: Box::new(err),
+        }
+    }
+}
+
+impl From<walkdir::Error> for SpandxError {
+    fn from(err: walkdir::Error) -> Self {
+        Self::FileSystemError {
+            operation: "directory walk".to_string(),
+            path: err.path().map(|p| p.display().to_string()).unwrap_or_else(|| "unknown".to_string()),
+            source: std::io::Error::new(std::io::ErrorKind::Other, err),
+        }
+    }
+}
+
+impl From<camino::FromPathBufError> for SpandxError {
+    fn from(err: camino::FromPathBufError) -> Self {
+        Self::ValidationError {
+            field: "path".to_string(),
+            reason: format!("Invalid UTF-8 path: {}", err),
+        }
+    }
+}
+
+impl From<indicatif::style::TemplateError> for SpandxError {
+    fn from(err: indicatif::style::TemplateError) -> Self {
+        Self::InternalError {
+            message: format!("Progress bar template error: {}", err),
+        }
+    }
+}
+
+/// Convenience macro for creating SpandxError with context
+#[macro_export]
+macro_rules! spandx_error {
+    ($variant:ident, $($field:ident = $value:expr),* $(,)?) => {
+        $crate::error::SpandxError::$variant {
+            $($field: $value.into(),)*
+        }
+    };
+}
+
+/// Convenience macro for early return with SpandxError
+#[macro_export]
+macro_rules! bail {
+    ($($arg:tt)*) => {
+        return Err($crate::error::SpandxError::InternalError {
+            message: format!($($arg)*),
+        });
+    };
+}
+
+/// Convenience macro for ensuring conditions
+#[macro_export]
+macro_rules! ensure {
+    ($cond:expr, $($arg:tt)*) => {
+        if !$cond {
+            $crate::bail!($($arg)*);
+        }
+    };
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_error_categories() {
+        let error = SpandxError::dependency_parse("test error");
+        assert_eq!(error.category(), ErrorCategory::Parse);
+        
+        let error = SpandxError::file_system("read", "/test/path", std::io::Error::from(std::io::ErrorKind::NotFound));
+        assert_eq!(error.category(), ErrorCategory::FileSystem);
+        
+        let error = SpandxError::cache("rebuild index");
+        assert_eq!(error.category(), ErrorCategory::Cache);
+    }
+    
+    #[test]
+    fn test_retriable_errors() {
+        // Test with a timeout error (retriable)
+        let error = SpandxError::RequestTimeout {
+            url: "https://example.com".to_string(),
+            timeout_ms: 30000,
+        };
+        assert!(error.is_retriable());
+        
+        let error = SpandxError::FileNotFound { 
+            path: "/test/path".to_string() 
+        };
+        assert!(!error.is_retriable());
+    }
+    
+    #[test]
+    fn test_user_messages() {
+        let error = SpandxError::PackageNotFound {
+            package: "react".to_string(),
+            version: "18.0.0".to_string(),
+            registry: "npm".to_string(),
+        };
+        assert_eq!(error.user_message(), "Package react@18.0.0 not found in npm");
+    }
+    
+    #[test]
+    fn test_error_constructors() {
+        let error = SpandxError::validation("version", "must be semver");
+        match error {
+            SpandxError::ValidationError { field, reason } => {
+                assert_eq!(field, "version");
+                assert_eq!(reason, "must be semver");
+            }
+            _ => panic!("Wrong error type"),
+        }
+    }
+}
\ No newline at end of file
src/lib.rs
@@ -0,0 +1,32 @@
+pub mod cli;
+pub mod core;
+pub mod parsers;
+pub mod formatters;
+pub mod spdx;
+pub mod cache;
+pub mod gateway;
+pub mod git;
+pub mod error;
+
+pub use core::*;
+pub use error::{SpandxError, SpandxResult};
+
+use std::sync::OnceLock;
+use tracing::Level;
+
+static AIRGAP_MODE: OnceLock<bool> = OnceLock::new();
+
+pub fn set_airgap_mode(airgap: bool) {
+    let _ = AIRGAP_MODE.set(airgap);
+}
+
+pub fn is_airgap_mode() -> bool {
+    AIRGAP_MODE.get().copied().unwrap_or(false)
+}
+
+pub fn init_tracing() {
+    tracing_subscriber::fmt()
+        .with_max_level(Level::INFO)
+        .with_target(false)
+        .init();
+}
\ No newline at end of file
src/main.rs
@@ -0,0 +1,190 @@
+use clap::Parser;
+use std::process;
+use tracing::{error, debug};
+
+use spandx::{SpandxError, SpandxResult};
+use spandx::cli::{Cli, Commands};
+use spandx::cli::commands::{ScanCommand, PullCommand, BuildCommand, VersionCommand};
+
+#[tokio::main]
+async fn main() {
+    if let Err(exit_code) = run().await {
+        process::exit(exit_code);
+    }
+}
+
+async fn run() -> Result<(), i32> {
+    let cli = Cli::parse();
+
+    // Initialize tracing based on log level
+    spandx::init_tracing();
+
+    let result: SpandxResult<()> = match cli.command {
+        Commands::Scan { 
+            path, 
+            recursive, 
+            airgap, 
+            logfile: _,  // TODO: Use logfile for tracing configuration
+            format, 
+            pull, 
+            require: _, // TODO: Implement module loading
+        } => {
+            let scan_cmd = ScanCommand::new(path, recursive, airgap, format, pull);
+            scan_cmd.execute().await
+        }
+
+        Commands::Pull => {
+            let pull_cmd = PullCommand::new();
+            pull_cmd.execute().await.map_err(|e| e.into())
+        }
+
+        Commands::Build { 
+            directory, 
+            logfile: _, // TODO: Use logfile for tracing configuration
+            index 
+        } => {
+            let build_cmd = BuildCommand::new(directory, index);
+            build_cmd.execute().await.map_err(|e| e.into())
+        }
+
+        Commands::Version => {
+            let version_cmd = VersionCommand::new();
+            version_cmd.execute().await.map_err(|e| e.into())
+        }
+    };
+
+    if let Err(e) = result {
+        handle_error(&e)
+    } else {
+        Ok(())
+    }
+}
+
+/// Enhanced error handling with user-friendly messages and proper exit codes
+fn handle_error(error: &SpandxError) -> Result<(), i32> {
+    // Log the full error for debugging
+    debug!("Full error details: {:?}", error);
+    
+    // Display user-friendly error message
+    eprintln!("Error: {}", error.user_message());
+    
+    // Show additional context for certain error types
+    match error {
+        SpandxError::FileNotFound { path } => {
+            eprintln!("  The file '{}' could not be found.", path);
+            eprintln!("  Please check the path and try again.");
+        }
+        SpandxError::DirectoryNotFound { path } => {
+            eprintln!("  The directory '{}' could not be found.", path);
+            eprintln!("  Please check the path and try again.");
+        }
+        SpandxError::PermissionDenied { path } => {
+            eprintln!("  Permission denied accessing '{}'.", path);
+            eprintln!("  Please check file permissions and try again.");
+        }
+        SpandxError::NetworkError { url, .. } => {
+            eprintln!("  Failed to access: {}", url);
+            eprintln!("  Please check your internet connection and try again.");
+            if error.is_retriable() {
+                if let Some(retry_ms) = error.retry_delay_ms() {
+                    eprintln!("  You can retry after {} seconds.", retry_ms / 1000);
+                }
+            }
+        }
+        SpandxError::PackageNotFound { package, version, registry } => {
+            eprintln!("  Package '{}@{}' not found in {}.", package, version, registry);
+            eprintln!("  Please verify the package name and version.");
+        }
+        SpandxError::InvalidArguments { .. } => {
+            eprintln!("  Use --help for usage information.");
+        }
+        SpandxError::ConfigError { .. } => {
+            eprintln!("  Check your configuration and try again.");
+        }
+        SpandxError::NotImplemented { feature } => {
+            eprintln!("  The feature '{}' is not yet implemented.", feature);
+            eprintln!("  Please check the documentation for supported features.");
+        }
+        _ => {
+            // For other errors, show category and suggest actions
+            eprintln!("  Category: {}", error.category());
+            
+            if error.is_retriable() {
+                eprintln!("  This error may be temporary. You can try again.");
+            } else {
+                eprintln!("  Please check the error details and fix any issues.");
+            }
+        }
+    }
+    
+    // Return appropriate exit code
+    let exit_code = match error.category() {
+        spandx::error::ErrorCategory::Cli => 2,        // Invalid usage
+        spandx::error::ErrorCategory::FileSystem => 3,  // File system issues
+        spandx::error::ErrorCategory::Network => 4,     // Network issues
+        spandx::error::ErrorCategory::Parse => 5,       // Parse errors
+        spandx::error::ErrorCategory::Config => 6,      // Configuration errors
+        _ => 1,                                          // General error
+    };
+    
+    error!("Command failed with error category: {} (exit code: {})", error.category(), exit_code);
+    Err(exit_code)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use assert_cmd::Command;
+    use predicates::prelude::*;
+
+    #[test]
+    fn test_version_command() {
+        let mut cmd = Command::cargo_bin("spandx").unwrap();
+        cmd.arg("version");
+        
+        cmd.assert()
+            .success()
+            .stdout(predicate::str::starts_with("v"));
+    }
+
+    #[test]
+    fn test_help_command() {
+        let mut cmd = Command::cargo_bin("spandx").unwrap();
+        cmd.arg("--help");
+        
+        cmd.assert()
+            .success()
+            .stdout(predicate::str::contains("spandx"))
+            .stdout(predicate::str::contains("Rust interface to the SPDX catalogue"));
+    }
+
+    #[test]
+    fn test_scan_help() {
+        let mut cmd = Command::cargo_bin("spandx").unwrap();
+        cmd.args(&["scan", "--help"]);
+        
+        cmd.assert()
+            .success()
+            .stdout(predicate::str::contains("Scan a lockfile"));
+    }
+
+    #[test]
+    fn test_pull_help() {
+        let mut cmd = Command::cargo_bin("spandx").unwrap();
+        cmd.args(&["pull", "--help"]);
+        
+        cmd.assert()
+            .success()
+            .stdout(predicate::str::contains("Pull the latest offline cache"));
+    }
+
+    #[test]
+    fn test_build_help() {
+        let mut cmd = Command::cargo_bin("spandx").unwrap();
+        cmd.args(&["build", "--help"]);
+        
+        cmd.assert()
+            .success()
+            .stdout(predicate::str::contains("Build a package index"));
+    }
+}
test_data/licenses/apache-2.0.txt
@@ -0,0 +1,71 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction,
+and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all
+other entities that control, are controlled by, or are under common
+control with that entity. For the purposes of this definition,
+"control" means (i) the power, direct or indirect, to cause the
+direction or management of such entity, whether by contract or
+otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity
+exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications,
+including but not limited to software source code, documentation
+source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical
+transformation or translation of a Source form, including but
+not limited to compiled object code, generated documentation,
+and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or
+Object form, made available under the License, as indicated by a
+copyright notice that is included in or attached to the work
+(provided that such notice shall not be construed as modifying
+the License).
+
+"Derivative Works" shall mean any work, whether in Source or Object
+form, that is based upon (or derived from) the Work and for which the
+editorial revisions, annotations, elaborations, or other modifications
+represent, as a whole, an original work of authorship. For the purposes
+of this License, Derivative Works shall not include works that remain
+separable from, or merely link (or bind by name) to the interfaces of,
+the Work and derivative works thereof.
+
+"Contribution" shall mean any work of authorship, including
+the original version of the Work and any modifications or additions
+to that Work or Derivative Works thereof, that is intentionally
+submitted to Licensor for inclusion in the Work by the copyright owner
+or by an individual or Legal Entity authorized to submit on behalf of
+the copyright owner. For the purposes of this definition, "submitted"
+means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control
+systems, and issue tracking systems that are managed by, or on behalf
+of, the Licensor for the purpose of discussing and improving the Work,
+but excluding communication that is conspicuously marked or otherwise
+designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity
+on behalf of whom a Contribution has been received by Licensor and
+subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+copyright license to use, reproduce, modify, display, perform,
+sublicense, and distribute the Work and such Derivative Works in
+Source or Object form.
\ No newline at end of file
test_data/licenses/gpl-3.0.txt
@@ -0,0 +1,38 @@
+GNU GENERAL PUBLIC LICENSE
+Version 3, 29 June 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+Everyone is permitted to copy and distribute verbatim copies
+of this license document, but changing it is not allowed.
+
+Preamble
+
+The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
\ No newline at end of file
test_data/licenses/mit.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) [year] [fullname]
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
tests/integration_tests.rs
@@ -0,0 +1,425 @@
+//! Integration Tests for Spandx
+//! 
+//! These tests verify end-to-end functionality of the complete system,
+//! including CLI commands, file parsing, caching, and output formatting.
+
+use assert_cmd::Command;
+use predicates::prelude::*;
+use std::fs;
+use tempfile::TempDir;
+
+/// Test that the CLI binary can be executed and shows help
+#[test]
+fn test_cli_help() {
+    let mut cmd = Command::cargo_bin("spandx").unwrap();
+    cmd.arg("--help");
+    
+    cmd.assert()
+        .success()
+        .stdout(predicate::str::contains("Rust interface to the SPDX catalogue"))
+        .stdout(predicate::str::contains("scan"))
+        .stdout(predicate::str::contains("pull"))
+        .stdout(predicate::str::contains("build"))
+        .stdout(predicate::str::contains("version"));
+}
+
+/// Test version command returns proper format
+#[test]
+fn test_version_command() {
+    let mut cmd = Command::cargo_bin("spandx").unwrap();
+    cmd.arg("version");
+    
+    cmd.assert()
+        .success()
+        .stdout(predicate::str::starts_with("v"));
+}
+
+/// Test scanning a valid Gemfile.lock
+#[test]
+fn test_scan_gemfile_lock() {
+    let temp_dir = TempDir::new().unwrap();
+    let gemfile_lock = temp_dir.path().join("Gemfile.lock");
+    
+    // Create a minimal Gemfile.lock
+    fs::write(&gemfile_lock, r#"
+GEM
+  remote: https://rubygems.org/
+  specs:
+    rack (2.2.3)
+    rails (7.0.0)
+      rack (>= 2.0.0)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  rails
+
+BUNDLED WITH
+   2.3.7
+"#).unwrap();
+    
+    let mut cmd = Command::cargo_bin("spandx").unwrap();
+    cmd.args(&["scan", gemfile_lock.to_str().unwrap(), "--airgap", "--format", "json"]);
+    
+    cmd.assert()
+        .success()
+        .stdout(predicate::str::contains("rack"))
+        .stdout(predicate::str::contains("rails"));
+}
+
+/// Test scanning with recursive directory search
+#[test]
+fn test_scan_recursive() {
+    let temp_dir = TempDir::new().unwrap();
+    let subdir = temp_dir.path().join("subproject");
+    fs::create_dir(&subdir).unwrap();
+    
+    let gemfile_lock = subdir.join("Gemfile.lock");
+    fs::write(&gemfile_lock, r#"
+GEM
+  remote: https://rubygems.org/
+  specs:
+    rake (13.0.6)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  rake
+
+BUNDLED WITH
+   2.3.7
+"#).unwrap();
+    
+    let mut cmd = Command::cargo_bin("spandx").unwrap();
+    cmd.args(&[
+        "scan", 
+        temp_dir.path().to_str().unwrap(), 
+        "--recursive",
+        "--airgap",
+        "--format", "table"
+    ]);
+    
+    cmd.assert()
+        .success()
+        .stdout(predicate::str::contains("rake"));
+}
+
+/// Test scanning non-existent file returns error
+#[test]
+fn test_scan_nonexistent_file() {
+    let mut cmd = Command::cargo_bin("spandx").unwrap();
+    cmd.args(&["scan", "/nonexistent/file.lock"]);
+    
+    cmd.assert()
+        .failure()
+        .stderr(predicate::str::contains("File not found"));
+}
+
+/// Test scanning with invalid format returns error
+#[test]
+fn test_scan_invalid_format() {
+    let temp_dir = TempDir::new().unwrap();
+    let gemfile_lock = temp_dir.path().join("Gemfile.lock");
+    fs::write(&gemfile_lock, "GEM\n").unwrap();
+    
+    let mut cmd = Command::cargo_bin("spandx").unwrap();
+    cmd.args(&[
+        "scan", 
+        gemfile_lock.to_str().unwrap(), 
+        "--format", "invalid_format"
+    ]);
+    
+    cmd.assert()
+        .failure()
+        .stderr(predicate::str::contains("invalid value"));
+}
+
+/// Test JSON output format contains expected fields
+#[test]
+fn test_json_output_format() {
+    let temp_dir = TempDir::new().unwrap();
+    let gemfile_lock = temp_dir.path().join("Gemfile.lock");
+    
+    fs::write(&gemfile_lock, r#"
+GEM
+  remote: https://rubygems.org/
+  specs:
+    minitest (5.15.0)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  minitest
+
+BUNDLED WITH
+   2.3.7
+"#).unwrap();
+    
+    let mut cmd = Command::cargo_bin("spandx").unwrap();
+    cmd.args(&[
+        "scan", 
+        gemfile_lock.to_str().unwrap(), 
+        "--airgap",
+        "--format", "json"
+    ]);
+    
+    let output = cmd.assert().success();
+    
+    // Parse the JSON output to verify structure
+    let stdout = String::from_utf8_lossy(&output.get_output().stdout);
+    if !stdout.trim().is_empty() {
+        match serde_json::from_str::<serde_json::Value>(&stdout) {
+            Ok(json) => {
+                assert!(json.is_array() || json.is_object());
+            }
+            Err(_) => {
+                // JSON parsing might fail for certain outputs, that's ok for now
+                // The important thing is that the command succeeded
+            }
+        }
+    }
+}
+
+/// Test CSV output format
+#[test]
+fn test_csv_output_format() {
+    let temp_dir = TempDir::new().unwrap();
+    let gemfile_lock = temp_dir.path().join("Gemfile.lock");
+    
+    fs::write(&gemfile_lock, r#"
+GEM
+  remote: https://rubygems.org/
+  specs:
+    json (2.6.1)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  json
+
+BUNDLED WITH
+   2.3.7
+"#).unwrap();
+    
+    let mut cmd = Command::cargo_bin("spandx").unwrap();
+    cmd.args(&[
+        "scan", 
+        gemfile_lock.to_str().unwrap(), 
+        "--airgap",
+        "--format", "csv"
+    ]);
+    
+    cmd.assert()
+        .success()
+        .stdout(predicate::str::contains("json"));
+}
+
+/// Test pull command
+#[test]
+fn test_pull_command() {
+    let mut cmd = Command::cargo_bin("spandx").unwrap();
+    cmd.arg("pull");
+    
+    // Pull might succeed or fail depending on network, but should not crash
+    let result = cmd.assert();
+    let output = result.get_output();
+    
+    // Should either succeed or fail gracefully with meaningful error
+    if output.status.success() {
+        // Success case - should have some output
+        assert!(!output.stdout.is_empty() || !output.stderr.is_empty());
+    } else {
+        // Failure case - should have meaningful error message
+        let stderr = String::from_utf8_lossy(&output.stderr);
+        assert!(
+            stderr.contains("Error:") || 
+            stderr.contains("Network") ||
+            stderr.contains("Git")
+        );
+    }
+}
+
+/// Test build command
+#[test]
+fn test_build_command() {
+    let temp_dir = TempDir::new().unwrap();
+    
+    let mut cmd = Command::cargo_bin("spandx").unwrap();
+    cmd.args(&["build", "--directory", temp_dir.path().to_str().unwrap()]);
+    
+    // Build might succeed or fail, but should not crash
+    let result = cmd.assert();
+    let output = result.get_output();
+    
+    // Should provide meaningful output either way
+    assert!(!output.stdout.is_empty() || !output.stderr.is_empty());
+}
+
+/// Test airgap mode prevents network access
+#[test]
+fn test_airgap_mode() {
+    let temp_dir = TempDir::new().unwrap();
+    let gemfile_lock = temp_dir.path().join("Gemfile.lock");
+    
+    fs::write(&gemfile_lock, r#"
+GEM
+  remote: https://rubygems.org/
+  specs:
+    some_remote_gem (1.0.0)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  some_remote_gem
+
+BUNDLED WITH
+   2.3.7
+"#).unwrap();
+    
+    let mut cmd = Command::cargo_bin("spandx").unwrap();
+    cmd.args(&[
+        "scan", 
+        gemfile_lock.to_str().unwrap(), 
+        "--airgap"
+    ]);
+    
+    // In airgap mode, should work but might have different license detection
+    cmd.assert().success();
+}
+
+/// Test conflicting arguments
+#[test]
+fn test_conflicting_arguments() {
+    let temp_dir = TempDir::new().unwrap();
+    let gemfile_lock = temp_dir.path().join("Gemfile.lock");
+    fs::write(&gemfile_lock, "GEM\n").unwrap();
+    
+    let mut cmd = Command::cargo_bin("spandx").unwrap();
+    cmd.args(&[
+        "scan", 
+        gemfile_lock.to_str().unwrap(), 
+        "--airgap", 
+        "--pull"
+    ]);
+    
+    // This should either work (pull ignored in airgap) or fail with clear error
+    let result = cmd.assert();
+    let output = result.get_output();
+    
+    if !output.status.success() {
+        let stderr = String::from_utf8_lossy(&output.stderr);
+        assert!(stderr.contains("airgap") || stderr.contains("pull"));
+    }
+}
+
+/// Test empty directory scan
+#[test]
+fn test_scan_empty_directory() {
+    let temp_dir = TempDir::new().unwrap();
+    
+    let mut cmd = Command::cargo_bin("spandx").unwrap();
+    cmd.args(&["scan", temp_dir.path().to_str().unwrap()]);
+    
+    cmd.assert()
+        .success(); // Should succeed but find no files
+}
+
+/// Test malformed Gemfile.lock handling
+#[test]
+fn test_malformed_gemfile_lock() {
+    let temp_dir = TempDir::new().unwrap();
+    let gemfile_lock = temp_dir.path().join("Gemfile.lock");
+    
+    // Write malformed content
+    fs::write(&gemfile_lock, "This is not a valid Gemfile.lock").unwrap();
+    
+    let mut cmd = Command::cargo_bin("spandx").unwrap();
+    cmd.args(&["scan", gemfile_lock.to_str().unwrap()]);
+    
+    // Should handle gracefully - either succeed with warnings or fail with clear error
+    let result = cmd.assert();
+    let output = result.get_output();
+    
+    // Should provide meaningful feedback
+    assert!(!output.stdout.is_empty() || !output.stderr.is_empty());
+}
+
+/// Test multiple file formats in same directory
+#[test]
+fn test_multiple_file_formats() {
+    let temp_dir = TempDir::new().unwrap();
+    
+    // Create Gemfile.lock
+    let gemfile_lock = temp_dir.path().join("Gemfile.lock");
+    fs::write(&gemfile_lock, r#"
+GEM
+  remote: https://rubygems.org/
+  specs:
+    rake (13.0.6)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  rake
+
+BUNDLED WITH
+   2.3.7
+"#).unwrap();
+    
+    // Create package-lock.json
+    let package_lock = temp_dir.path().join("package-lock.json");
+    fs::write(&package_lock, r#"
+{
+  "name": "test-project",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "version": "1.0.0"
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21"
+    }
+  },
+  "dependencies": {
+    "lodash": {
+      "version": "4.17.21"
+    }
+  }
+}
+"#).unwrap();
+    
+    let mut cmd = Command::cargo_bin("spandx").unwrap();
+    cmd.args(&[
+        "scan", 
+        temp_dir.path().to_str().unwrap(),
+        "--recursive",
+        "--format", "json"
+    ]);
+    
+    cmd.assert()
+        .success()
+        .stdout(predicate::str::contains("rake").or(predicate::str::contains("lodash")));
+}
+
+/// Test that help for each subcommand works
+#[test]
+fn test_subcommand_help() {
+    let subcommands = ["scan", "pull", "build", "version"];
+    
+    for subcmd in &subcommands {
+        let mut cmd = Command::cargo_bin("spandx").unwrap();
+        cmd.args(&[*subcmd, "--help"]);
+        
+        cmd.assert()
+            .success()
+            .stdout(predicate::str::contains(*subcmd));
+    }
+}
\ No newline at end of file
.gitignore
@@ -6,6 +6,7 @@
 /pkg/
 /spec/reports/
 /tmp/
+/target/
 *.so
 *.bundle
 
Cargo.lock
@@ -0,0 +1,3620 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "ahash"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
+dependencies = [
+ "getrandom 0.2.16",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anes"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
+
+[[package]]
+name = "anstream"
+version = "0.6.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+
+[[package]]
+name = "assert-json-diff"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "assert_cmd"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66"
+dependencies = [
+ "anstyle",
+ "bstr",
+ "doc-comment",
+ "libc",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener",
+ "futures-core",
+]
+
+[[package]]
+name = "async-stream"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bstr"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
+dependencies = [
+ "memchr",
+ "regex-automata 0.4.9",
+ "serde",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "bytecount"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "camino"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cast"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+
+[[package]]
+name = "cc"
+version = "1.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
+dependencies = [
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+
+[[package]]
+name = "chrono"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "ciborium"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
+
+[[package]]
+name = "ciborium-io"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
+
+[[package]]
+name = "ciborium-ll"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
+dependencies = [
+ "ciborium-io",
+ "half",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim 0.11.1",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "config"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca"
+dependencies = [
+ "async-trait",
+ "json5",
+ "lazy_static",
+ "nom",
+ "pathdiff",
+ "ron",
+ "rust-ini",
+ "serde",
+ "serde_json",
+ "toml 0.5.11",
+ "yaml-rust",
+]
+
+[[package]]
+name = "console"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "once_cell",
+ "unicode-width 0.2.1",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "criterion"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
+dependencies = [
+ "anes",
+ "cast",
+ "ciborium",
+ "clap",
+ "criterion-plot",
+ "is-terminal",
+ "itertools",
+ "num-traits",
+ "once_cell",
+ "oorandom",
+ "plotters",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "tinytemplate",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
+dependencies = [
+ "cast",
+ "itertools",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "csv"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
+dependencies = [
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "deadpool"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e"
+dependencies = [
+ "async-trait",
+ "deadpool-runtime",
+ "num_cpus",
+ "retain_mut",
+ "tokio",
+]
+
+[[package]]
+name = "deadpool-runtime"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
+
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "dlv-list"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
+
+[[package]]
+name = "doc-comment"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "flate2"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "float-cmp"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-lite"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
+dependencies = [
+ "fastrand 1.9.0",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite",
+ "waker-fn",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-timer"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "git2"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70"
+dependencies = [
+ "bitflags 2.9.1",
+ "libc",
+ "libgit2-sys",
+ "log",
+ "openssl-probe",
+ "openssl-sys",
+ "url",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "half"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
+
+[[package]]
+name = "hcl-edit"
+version = "0.7.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "191e0335c5fda9cb8a028af95a73635e781970f2af1df55d9f49b4331dfc4315"
+dependencies = [
+ "fnv",
+ "hcl-primitives",
+ "vecmap-rs",
+ "winnow 0.6.26",
+]
+
+[[package]]
+name = "hcl-primitives"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f097693bfc799cc5043956e93a28c51ca4e72f2c3daa21f65a5b0a28510df1f2"
+dependencies = [
+ "itoa",
+ "kstring",
+ "ryu",
+ "serde",
+ "unicode-ident",
+]
+
+[[package]]
+name = "hcl-rs"
+version = "0.16.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a53aa7b895ddc4bf4eeb6502bc4db0256897ae7cfede76155ddd29352518dfee"
+dependencies = [
+ "hcl-edit",
+ "hcl-primitives",
+ "indexmap",
+ "itoa",
+ "serde",
+ "vecmap-rs",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http-types"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad"
+dependencies = [
+ "anyhow",
+ "async-channel",
+ "base64 0.13.1",
+ "futures-lite",
+ "http",
+ "infer",
+ "pin-project-lite",
+ "rand",
+ "serde",
+ "serde_json",
+ "serde_qs",
+ "serde_urlencoded",
+ "url",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "0.14.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.15.4",
+ "serde",
+]
+
+[[package]]
+name = "indicatif"
+version = "0.17.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4adb2ee6ad319a912210a36e56e3623555817bcc877a7e6e8802d1d69c4d8056"
+dependencies = [
+ "console",
+ "portable-atomic",
+ "unicode-width 0.2.1",
+ "unit-prefix",
+ "web-time",
+]
+
+[[package]]
+name = "infer"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac"
+
+[[package]]
+name = "instant"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
+[[package]]
+name = "is-terminal"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "jobserver"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
+dependencies = [
+ "getrandom 0.3.3",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json5"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
+dependencies = [
+ "pest",
+ "pest_derive",
+ "serde",
+]
+
+[[package]]
+name = "kstring"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1"
+dependencies = [
+ "serde",
+ "static_assertions",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.174"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+
+[[package]]
+name = "libgit2-sys"
+version = "0.16.2+1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8"
+dependencies = [
+ "cc",
+ "libc",
+ "libssh2-sys",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+]
+
+[[package]]
+name = "libredox"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
+dependencies = [
+ "bitflags 2.9.1",
+ "libc",
+]
+
+[[package]]
+name = "libssh2-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linked-hash-map"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
+name = "lock_api"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata 0.1.10",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
+[[package]]
+name = "oorandom"
+version = "11.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
+
+[[package]]
+name = "openssl"
+version = "0.10.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+dependencies = [
+ "bitflags 2.9.1",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "ordered-multimap"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a"
+dependencies = [
+ "dlv-list",
+ "hashbrown 0.12.3",
+]
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "papergrid"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2ccbe15f2b6db62f9a9871642746427e297b0ceb85f9a7f1ee5ff47d184d0c8"
+dependencies = [
+ "bytecount",
+ "fnv",
+ "unicode-width 0.1.14",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core 0.8.6",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+dependencies = [
+ "lock_api",
+ "parking_lot_core 0.9.11",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall 0.2.16",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.5.13",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pest"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
+dependencies = [
+ "memchr",
+ "thiserror 2.0.12",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
+dependencies = [
+ "pest",
+ "sha2",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "plotters"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
+dependencies = [
+ "num-traits",
+ "plotters-backend",
+ "plotters-svg",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "plotters-backend"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
+
+[[package]]
+name = "plotters-svg"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
+dependencies = [
+ "plotters-backend",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "predicates"
+version = "3.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
+dependencies = [
+ "anstyle",
+ "difflib",
+ "float-cmp",
+ "normalize-line-endings",
+ "predicates-core",
+ "regex",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "rayon"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
+dependencies = [
+ "bitflags 2.9.1",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom 0.2.16",
+ "libredox",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata 0.4.9",
+ "regex-syntax 0.8.5",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax 0.6.29",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax 0.8.5",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "reqwest"
+version = "0.11.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
+dependencies = [
+ "base64 0.21.7",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pemfile",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "system-configuration",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-util",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
+name = "retain_mut"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0"
+
+[[package]]
+name = "ron"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
+dependencies = [
+ "base64 0.13.1",
+ "bitflags 1.3.2",
+ "serde",
+]
+
+[[package]]
+name = "roxmltree"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
+
+[[package]]
+name = "rust-ini"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df"
+dependencies = [
+ "cfg-if",
+ "ordered-multimap",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
+
+[[package]]
+name = "rustix"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+dependencies = [
+ "bitflags 2.9.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
+dependencies = [
+ "base64 0.21.7",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.9.1",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_qs"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6"
+dependencies = [
+ "percent-encoding",
+ "serde",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
+
+[[package]]
+name = "sled"
+version = "0.34.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
+dependencies = [
+ "crc32fast",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "fs2",
+ "fxhash",
+ "libc",
+ "log",
+ "parking_lot 0.11.2",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "spandx"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "assert_cmd",
+ "async-trait",
+ "byteorder",
+ "camino",
+ "chrono",
+ "clap",
+ "config",
+ "criterion",
+ "csv",
+ "dirs",
+ "flate2",
+ "futures",
+ "git2",
+ "hcl-rs",
+ "indicatif",
+ "lazy_static",
+ "pest",
+ "pest_derive",
+ "predicates",
+ "quick-xml",
+ "rayon",
+ "regex",
+ "reqwest",
+ "roxmltree",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "sha1",
+ "sled",
+ "strsim 0.10.0",
+ "tabled",
+ "tempfile",
+ "thiserror 1.0.69",
+ "tokio",
+ "tokio-test",
+ "toml 0.8.23",
+ "tracing",
+ "tracing-subscriber",
+ "url",
+ "urlencoding",
+ "uuid",
+ "walkdir",
+ "wiremock",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tabled"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfe9c3632da101aba5131ed63f9eed38665f8b3c68703a6bb18124835c1a5d22"
+dependencies = [
+ "papergrid",
+ "tabled_derive",
+ "unicode-width 0.1.14",
+]
+
+[[package]]
+name = "tabled_derive"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99f688a08b54f4f02f0a3c382aefdb7884d3d69609f785bd253dc033243e3fe4"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
+dependencies = [
+ "fastrand 2.3.0",
+ "getrandom 0.3.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "termtree"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl 2.0.12",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinytemplate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "tokio"
+version = "1.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot 0.12.4",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-test"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7"
+dependencies = [
+ "async-stream",
+ "bytes",
+ "futures-core",
+ "tokio",
+ "tokio-stream",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow 0.7.11",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
+
+[[package]]
+name = "unit-prefix"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817"
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
+[[package]]
+name = "url"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
+dependencies = [
+ "getrandom 0.3.3",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "vecmap-rs"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67cfc542f75493f412a51c02af26f58f710ab0e2204d264135054377244276be"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "waker-fn"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
+name = "winnow"
+version = "0.6.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "wiremock"
+version = "0.5.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13a3a53eaf34f390dd30d7b1b078287dd05df2aa2e21a589ccb80f5c7253c2e9"
+dependencies = [
+ "assert-json-diff",
+ "async-trait",
+ "base64 0.21.7",
+ "deadpool",
+ "futures",
+ "futures-timer",
+ "http-types",
+ "hyper",
+ "log",
+ "once_cell",
+ "regex",
+ "serde",
+ "serde_json",
+ "tokio",
+]
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags 2.9.1",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "synstructure",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
Cargo.toml
@@ -0,0 +1,144 @@
+[package]
+name = "spandx"
+version = "0.1.0"
+edition = "2021"
+rust-version = "1.70"
+authors = ["Can Eldem <eldemcan@gmail.com>", "mo khan <mo@mokhan.ca>"]
+description = "A Rust interface to the SPDX catalogue for dependency license scanning"
+homepage = "https://spandx.github.io/"
+repository = "https://github.com/spandx/spandx-rs"
+license = "MIT"
+keywords = ["spdx", "license", "dependencies", "security", "scanner"]
+categories = ["command-line-utilities", "development-tools"]
+
+[dependencies]
+# CLI framework
+clap = { version = "4.0", features = ["derive", "env"] }
+
+# HTTP client
+reqwest = { version = "0.11", features = ["json", "stream"] }
+tokio = { version = "1.0", features = ["full"] }
+
+# Serialization
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+serde_yaml = "0.9"
+toml = "0.8"
+
+# XML parsing
+roxmltree = "0.19"
+quick-xml = { version = "0.31", features = ["serialize"] }
+
+# Git operations
+git2 = "0.18"
+
+# Error handling
+anyhow = "1.0"
+thiserror = "1.0"
+
+# Async runtime and utilities
+futures = "0.3"
+async-trait = "0.1"
+
+# Parallel processing
+rayon = "1.8"
+
+# Path handling
+camino = { version = "1.1", features = ["serde1"] }
+
+# Progress indicators
+indicatif = "0.17"
+
+# Table formatting
+tabled = "0.14"
+
+# CSV handling
+csv = "1.3"
+
+# Logging
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+
+# URL handling
+url = { version = "2.4", features = ["serde"] }
+
+# Regular expressions
+regex = "1.10"
+
+# String similarity
+strsim = "0.10"
+
+# Configuration
+config = "0.13"
+
+# File watching and utilities
+walkdir = "2.4"
+tempfile = "3.8"
+
+# Compression
+flate2 = "1.0"
+
+# Cache storage
+sled = "0.34"
+
+# HCL parsing for Terraform
+hcl-rs = "0.16"
+
+# License expression parsing
+pest = "2.7"
+pest_derive = "2.7"
+
+# Binary data handling
+byteorder = "1.5"
+
+# Time handling
+chrono = { version = "0.4", features = ["serde"] }
+
+# UUID generation
+uuid = { version = "1.6", features = ["v4"] }
+
+# URL encoding
+urlencoding = "2.1"
+
+# Hashing
+sha1 = "0.10"
+
+# Directory utilities
+dirs = "5.0"
+
+# Static values
+lazy_static = "1.4"
+
+[dev-dependencies]
+# Testing
+tokio-test = "0.4"
+wiremock = "0.5"
+assert_cmd = "2.0"
+predicates = "3.0"
+tempfile = "3.8"
+criterion = { version = "0.5", features = ["html_reports"] }
+
+[build-dependencies]
+# Build-time dependencies if needed
+
+[[bin]]
+name = "spandx"
+path = "src/main.rs"
+
+[[example]]
+name = "hierarchical_cache_demo"
+path = "examples/hierarchical_cache_demo.rs"
+
+[[example]]
+name = "error_handling_demo"
+path = "examples/error_handling_demo.rs"
+
+[profile.release]
+lto = true
+codegen-units = 1
+panic = "abort"
+strip = true
+
+[[bench]]
+name = "performance_benchmarks"
+harness = false