## Coverage Report

#### Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/realizar/src/uri.rs

    Line
    Count
    Source
    1
    //! Pacha URI scheme support for model loading
    2
    //!
    3
    //! Enables direct model loading from the Pacha registry using URIs like:
    4
    //! - `pacha://model-name:version`
    5
    //! - `pacha://model-name` (latest version)
    6
    //!
    7
    //! ## Example
    8
    //!
    9
    //! ```rust,ignore
    10
    //! use realizar::uri::{PachaUri, resolve_model_uri};
    11
    //!
    12
    //! let uri = PachaUri::parse("pacha://llama-7b:1.0.0")?;
    13
    //! let model_path = resolve_model_uri(&uri).await?;
    14
    //! ```
    15
    //!
    16
    //! ## Integration
    17
    //!
    18
    //! The Pacha URI scheme integrates with:
    19
    //! - Model registry for automatic metadata retrieval
    20
    //! - Lineage tracking for inference metrics
    21
    //! - Content addressing for integrity verification
    22
    23
    #[cfg(feature = "registry")]
    24
    use std::path::PathBuf;
    25
    26
    use crate::error::{RealizarError, Result};
    27
    28
    /// Parsed Pacha URI
    29
    ///
    30
    /// Represents a `pacha://model:version` URI for model loading.
    31
    #[derive(Debug, Clone, PartialEq, Eq)]
    32
    pub struct PachaUri {
    33
        /// Model name/identifier
    34
        pub model: String,
    35
        /// Version (None means latest)
    36
        pub version: Option<String>,
    37
    }
    38
    39
    impl PachaUri {
    40
        /// Parse a Pacha URI string
    41
        ///
    42
        /// Supported formats:
    43
        /// - `pacha://model-name:version`
    44
        /// - `pacha://model-name` (latest version)
    45
        ///
    46
        /// # Errors
    47
        ///
    48
        /// Returns error if URI is malformed or uses unsupported scheme
    49
    21
        pub fn parse(uri: &str) -> Result<Self> {
    50
            // Check for pacha:// scheme
    51
    21
            let path20 = uri
    52
    21
                .strip_prefix("pacha://")
    53
    21
                .ok_or_else(|| RealizarError::InvalidUri(format!1("Expected pacha:// scheme: {uri}"1)))?1;
    54
    55
    20
            if path.is_empty() {
    56
    2
                return Err(RealizarError::InvalidUri(
    57
    2
                    "Model name required after pacha://".to_string(),
    58
    2
                ));
    59
    18
            }
    60
    61
            // Split model:version
    62
    18
            let (model16, version16) = if let Some(colon_pos13) = path.rfind(':') {
    63
    13
                let model = &path[..colon_pos];
    64
    13
                let version = &path[colon_pos + 1..];
    65
    66
    13
                if model.is_empty() {
    67
    1
                    return Err(RealizarError::InvalidUri(
    68
    1
                        "Model name cannot be empty".to_string(),
    69
    1
                    ));
    70
    12
                }
    71
    12
                if version.is_empty() {
    72
    1
                    return Err(RealizarError::InvalidUri(
    73
    1
                        "Version cannot be empty after colon".to_string(),
    74
    1
                    ));
    75
    11
                }
    76
    77
    11
                (model.to_string(), Some(version.to_string()))
    78
            } else {
    79
    5
                (path.to_string(), None)
    80
            };
    81
    82
    16
            Ok(Self { model, version })
    83
    21
        }
    84
    85
        /// Check if this is a Pacha URI
    86
        #[must_use]
    87
    4
        pub fn is_pacha_uri(uri: &str) -> bool {
    88
    4
            uri.starts_with("pacha://")
    89
    4
        }
    90
    91
        /// Convert back to URI string
    92
        #[must_use]
    93
    8
        pub fn to_uri_string(&self) -> String {
    94
    8
            match &self.version {
    95
    5
                Some(v) => format!("pacha://{}:{v}", self.model),
    96
    3
                None => format!("pacha://{}", self.model),
    97
            }
    98
    8
        }
    99
    100
        /// Get the version or "latest" if none specified
    101
        #[must_use]
    102
    2
        pub fn version_or_latest(&self) -> &str {
    103
    2
            self.version.as_deref().unwrap_or("latest")
    104
    2
        }
    105
    }
    106
    107
    impl std::fmt::Display for PachaUri {
    108
    1
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    109
    1
            write!(f, "{}", self.to_uri_string())
    110
    1
        }
    111
    }
    112
    113
    /// Model metadata retrieved from Pacha registry
    114
    #[derive(Debug, Clone)]
    115
    #[cfg(feature = "registry")]
    116
    pub struct ModelMetadata {
    117
        /// Model name
    118
        pub name: String,
    119
        /// Model version
    120
        pub version: String,
    121
        /// Content hash (BLAKE3)
    122
        pub content_hash: String,
    123
        /// Signer public key (Ed25519) if signed
    124
        pub signer_key: Option<String>,
    125
        /// Local cached path
    126
        pub local_path: PathBuf,
    127
    }
    128
    129
    /// Resolver for Pacha URIs
    130
    ///
    131
    /// Handles model resolution from the Pacha registry.
    132
    #[cfg(feature = "registry")]
    133
    pub struct PachaResolver {
    134
        registry: pacha::Registry,
    135
    }
    136
    137
    #[cfg(feature = "registry")]
    138
    impl PachaResolver {
    139
        /// Create a new resolver with default registry
    140
        ///
    141
        /// # Errors
    142
        ///
    143
        /// Returns error if registry cannot be opened
    144
        pub fn new() -> Result<Self> {
    145
            let registry = pacha::Registry::open_default()
    146
                .map_err(|e| RealizarError::RegistryError(format!("Failed to open registry: {e}")))?;
    147
            Ok(Self { registry })
    148
        }
    149
    150
        /// Create a resolver with a specific registry
    151
        #[must_use]
    152
        pub fn with_registry(registry: pacha::Registry) -> Self {
    153
            Self { registry }
    154
        }
    155
    156
        /// Resolve a Pacha URI to model metadata and artifact data
    157
        ///
    158
        /// This function:
    159
        /// 1. Parses the Pacha URI
    160
        /// 2. Retrieves model from registry
    161
        /// 3. Returns the metadata and artifact data
    162
        ///
    163
        /// # Errors
    164
        ///
    165
        /// Returns error if:
    166
        /// - Model not found in registry
    167
        /// - Retrieval fails
    168
        pub fn resolve(&self, uri: &PachaUri) -> Result<(ModelMetadata, Vec<u8>)> {
    169
            use pacha::model::ModelVersion;
    170
    171
            // Parse version
    172
            let version = match &uri.version {
    173
                Some(v) => ModelVersion::parse(v)
    174
                    .map_err(|e| RealizarError::RegistryError(format!("Invalid version: {e}")))?,
    175
                None => ModelVersion::new(0, 0, 0), // Will get latest
    176
            };
    177
    178
            // Get model metadata from registry
    179
            let model = self
    180
                .registry
    181
                .get_model(&uri.model, &version)
    182
                .map_err(|e| RealizarError::ModelNotFound(format!("{}: {e}", uri.model)))?;
    183
    184
            // Get the artifact data
    185
            let artifact = self
    186
                .registry
    187
                .get_model_artifact(&uri.model, &version)
    188
                .map_err(|e| {
    189
                    RealizarError::RegistryError(format!("Failed to get model artifact: {e}"))
    190
                })?;
    191
    192
            let metadata = ModelMetadata {
    193
                name: uri.model.clone(),
    194
                version: model.version.to_string(),
    195
                content_hash: model.content_address.to_string(),
    196
                signer_key: None, // Signing info retrieved separately via pacha::signing
    197
                local_path: PathBuf::new(), // Artifact is returned directly, no file path
    198
            };
    199
    200
            Ok((metadata, artifact))
    201
        }
    202
    203
        /// Get model metadata without loading artifact
    204
        ///
    205
        /// # Errors
    206
        ///
    207
        /// Returns error if model not found
    208
        pub fn get_metadata(&self, uri: &PachaUri) -> Result<ModelMetadata> {
    209
            use pacha::model::ModelVersion;
    210
    211
            let version = match &uri.version {
    212
                Some(v) => ModelVersion::parse(v)
    213
                    .map_err(|e| RealizarError::RegistryError(format!("Invalid version: {e}")))?,
    214
                None => ModelVersion::new(0, 0, 0),
    215
            };
    216
    217
            let model = self
    218
                .registry
    219
                .get_model(&uri.model, &version)
    220
                .map_err(|e| RealizarError::ModelNotFound(format!("{}: {e}", uri.model)))?;
    221
    222
            Ok(ModelMetadata {
    223
                name: uri.model.clone(),
    224
                version: model.version.to_string(),
    225
                content_hash: model.content_address.to_string(),
    226
                signer_key: None, // Signing info retrieved separately via pacha::signing
    227
                local_path: PathBuf::new(),
    228
            })
    229
        }
    230
    }
    231
    232
    /// Lineage information for model tracing
    233
    ///
    234
    /// Propagated to inference metrics for observability.
    235
    #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
    236
    pub struct ModelLineage {
    237
        /// Original Pacha URI
    238
        pub uri: String,
    239
        /// Model name
    240
        pub model: String,
    241
        /// Model version
    242
        pub version: String,
    243
        /// Content hash (BLAKE3) for integrity
    244
        pub content_hash: String,
    245
        /// Whether the model was verified (signature check)
    246
        pub verified: bool,
    247
        /// Timestamp when lineage was captured
    248
        pub captured_at: u64,
    249
    }
    250
    251
    impl ModelLineage {
    252
        /// Create lineage from Pacha URI and metadata
    253
        #[cfg(feature = "registry")]
    254
        #[must_use]
    255
        pub fn from_metadata(uri: &PachaUri, metadata: &ModelMetadata) -> Self {
    256
            Self {
    257
                uri: uri.to_uri_string(),
    258
                model: metadata.name.clone(),
    259
                version: metadata.version.clone(),
    260
                content_hash: metadata.content_hash.clone(),
    261
                verified: metadata.signer_key.is_some(),
    262
                captured_at: std::time::SystemTime::now()
    263
                    .duration_since(std::time::UNIX_EPOCH)
    264
                    .map(|d| d.as_secs())
    265
                    .unwrap_or(0),
    266
            }
    267
        }
    268
    269
        /// Create lineage without registry metadata
    270
        #[must_use]
    271
    5
        pub fn from_uri(uri: &PachaUri) -> Self {
    272
            Self {
    273
    5
                uri: uri.to_uri_string(),
    274
    5
                model: uri.model.clone(),
    275
    5
                version: uri.version.clone().unwrap_or_else(|| "unknown"2.to_string2()),
    276
    5
                content_hash: String::new(),
    277
                verified: false,
    278
    5
                captured_at: std::time::SystemTime::now()
    279
    5
                    .duration_since(std::time::UNIX_EPOCH)
    280
    5
                    .map(|d| d.as_secs())
    281
    5
                    .unwrap_or(0),
    282
            }
    283
    5
        }
    284
    }
    285
    286
    #[cfg(test)]
    287
    mod tests {
    288
        use super::*;
    289
    290
        #[test]
    291
    1
        fn test_parse_full_uri() {
    292
    1
            let uri = PachaUri::parse("pacha://llama-7b:1.0.0").expect("test");
    293
    1
            assert_eq!(uri.model, "llama-7b");
    294
    1
            assert_eq!(uri.version, Some("1.0.0".to_string()));
    295
    1
        }
    296
    297
        #[test]
    298
    1
        fn test_parse_uri_without_version() {
    299
    1
            let uri = PachaUri::parse("pacha://mistral-7b").expect("test");
    300
    1
            assert_eq!(uri.model, "mistral-7b");
    301
    1
            assert_eq!(uri.version, None);
    302
    1
        }
    303
    304
        #[test]
    305
    1
        fn test_parse_uri_with_semver() {
    306
    1
            let uri = PachaUri::parse("pacha://gpt2:2.0.0-beta.1").expect("test");
    307
    1
            assert_eq!(uri.model, "gpt2");
    308
    1
            assert_eq!(uri.version, Some("2.0.0-beta.1".to_string()));
    309
    1
        }
    310
    311
        #[test]
    312
    1
        fn test_parse_uri_with_org() {
    313
    1
            let uri = PachaUri::parse("pacha://paiml/trueno-llm:1.0").expect("test");
    314
    1
            assert_eq!(uri.model, "paiml/trueno-llm");
    315
    1
            assert_eq!(uri.version, Some("1.0".to_string()));
    316
    1
        }
    317
    318
        #[test]
    319
    1
        fn test_invalid_scheme() {
    320
    1
            let result = PachaUri::parse("http://example.com/model");
    321
    1
            assert!(result.is_err());
    322
    1
        }
    323
    324
        #[test]
    325
    1
        fn test_empty_model() {
    326
    1
            let result = PachaUri::parse("pacha://");
    327
    1
            assert!(result.is_err());
    328
    1
        }
    329
    330
        #[test]
    331
    1
        fn test_empty_version() {
    332
    1
            let result = PachaUri::parse("pacha://model:");
    333
    1
            assert!(result.is_err());
    334
    1
        }
    335
    336
        #[test]
    337
    1
        fn test_is_pacha_uri() {
    338
    1
            assert!(PachaUri::is_pacha_uri("pacha://model:1.0"));
    339
    1
            assert!(PachaUri::is_pacha_uri("pacha://model"));
    340
    1
            assert!(!PachaUri::is_pacha_uri("http://example.com"));
    341
    1
            assert!(!PachaUri::is_pacha_uri("/path/to/model.gguf"));
    342
    1
        }
    343
    344
        #[test]
    345
    1
        fn test_to_uri_string() {
    346
    1
            let uri = PachaUri::parse("pacha://model:1.0").expect("test");
    347
    1
            assert_eq!(uri.to_uri_string(), "pacha://model:1.0");
    348
    349
    1
            let uri_no_version = PachaUri::parse("pacha://model").expect("test");
    350
    1
            assert_eq!(uri_no_version.to_uri_string(), "pacha://model");
    351
    1
        }
    352
    353
        #[test]
    354
    1
        fn test_version_or_latest() {
    355
    1
            let uri = PachaUri::parse("pacha://model:2.0").expect("test");
    356
    1
            assert_eq!(uri.version_or_latest(), "2.0");
    357
    358
    1
            let uri_no_version = PachaUri::parse("pacha://model").expect("test");
    359
    1
            assert_eq!(uri_no_version.version_or_latest(), "latest");
    360
    1
        }
    361
    362
        #[test]
    363
    1
        fn test_display() {
    364
    1
            let uri = PachaUri::parse("pacha://llama:1.0.0").expect("test");
    365
    1
            assert_eq!(format!("{uri}"), "pacha://llama:1.0.0");
    366
    1
        }
    367
    368
        #[test]
    369
    1
        fn test_lineage_from_uri() {
    370
    1
            let uri = PachaUri::parse("pacha://model:1.0").expect("test");
    371
    1
            let lineage = ModelLineage::from_uri(&uri);
    372
    373
    1
            assert_eq!(lineage.uri, "pacha://model:1.0");
    374
    1
            assert_eq!(lineage.model, "model");
    375
    1
            assert_eq!(lineage.version, "1.0");
    376
    1
            assert!(!lineage.verified);
    377
    1
            assert!(lineage.captured_at > 0);
    378
    1
        }
    379
    380
        // =========================================================================
    381
        // Coverage Tests
    382
        // =========================================================================
    383
    384
        #[test]
    385
    1
        fn test_pacha_uri_clone_and_eq() {
    386
    1
            let uri1 = PachaUri::parse("pacha://model:1.0").expect("test");
    387
    1
            let uri2 = uri1.clone();
    388
    1
            assert_eq!(uri1, uri2);
    389
    1
        }
    390
    391
        #[test]
    392
    1
        fn test_lineage_from_uri_no_version() {
    393
    1
            let uri = PachaUri::parse("pacha://model-name").expect("test");
    394
    1
            let lineage = ModelLineage::from_uri(&uri);
    395
    1
            assert_eq!(lineage.version, "unknown");
    396
    1
        }
    397
    398
        #[test]
    399
    1
        fn test_parse_empty_model_error_msg() {
    400
    1
            let result = PachaUri::parse("pacha://");
    401
    1
            match result {
    402
    1
                Err(e) => assert!(e.to_string().contains("required")),
    403
    0
                Ok(_) => panic!("Expected error"),
    404
            }
    405
    1
        }
    406
    407
        #[test]
    408
    1
        fn test_parse_empty_model_name_before_colon() {
    409
    1
            let result = PachaUri::parse("pacha://:1.0");
    410
    1
            match result {
    411
    1
                Err(e) => assert!(e.to_string().contains("empty")),
    412
    0
                Ok(_) => panic!("Expected error"),
    413
            }
    414
    1
        }
    415
    416
        #[test]
    417
    1
        fn test_lineage_content_hash_empty() {
    418
    1
            let uri = PachaUri::parse("pacha://test").expect("test");
    419
    1
            let lineage = ModelLineage::from_uri(&uri);
    420
    1
            assert!(lineage.content_hash.is_empty());
    421
    1
        }
    422
    423
        #[test]
    424
    1
        fn test_model_lineage_serialize() {
    425
    1
            let uri = PachaUri::parse("pacha://model:1.0").expect("test");
    426
    1
            let lineage = ModelLineage::from_uri(&uri);
    427
    1
            let json = serde_json::to_string(&lineage).expect("test");
    428
    1
            assert!(json.contains("model"));
    429
    1
            assert!(json.contains("1.0"));
    430
    1
        }
    431
    432
        #[test]
    433
    1
        fn test_model_lineage_deserialize() {
    434
    1
            let json = r#"{"uri":"pacha://test:1.0","model":"test","version":"1.0","content_hash":"","verified":false,"captured_at":12345}"#;
    435
    1
            let lineage: ModelLineage = serde_json::from_str(json).expect("test");
    436
    1
            assert_eq!(lineage.model, "test");
    437
    1
            assert_eq!(lineage.version, "1.0");
    438
    1
        }
    439
    440
        #[test]
    441
    1
        fn test_pacha_uri_debug() {
    442
    1
            let uri = PachaUri::parse("pacha://model:1.0").expect("test");
    443
    1
            let debug = format!("{:?}", uri);
    444
    1
            assert!(debug.contains("PachaUri"));
    445
    1
            assert!(debug.contains("model"));
    446
    1
        }
    447
    448
        #[test]
    449
    1
        fn test_model_lineage_debug() {
    450
    1
            let uri = PachaUri::parse("pacha://model:1.0").expect("test");
    451
    1
            let lineage = ModelLineage::from_uri(&uri);
    452
    1
            let debug = format!("{:?}", lineage);
    453
    1
            assert!(debug.contains("ModelLineage"));
    454
    1
        }
    455
    }