Skip to main content

sqry_classpath/graph/
provenance.rs

1//! Provenance tracking types for classpath entries.
2//!
3//! Tracks the origin and dependency status of each JAR on the classpath,
4//! enabling FQN precedence decisions (workspace > direct > transitive) and
5//! metadata attachment to emitted graph nodes.
6
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11/// Module/root-level scope membership for a JAR.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct ClasspathScope {
14    /// Logical module name (may be empty for root projects).
15    pub module_name: String,
16    /// Concrete module root path used for importer scoping.
17    pub module_root: PathBuf,
18    /// Whether the JAR is direct within this scope.
19    pub is_direct: bool,
20}
21
22/// Tracks the origin and dependency status of a classpath entry.
23///
24/// Used during graph emission to:
25/// 1. Set `is_direct_dependency` on [`ClasspathNodeMetadata`].
26/// 2. Control `ExportMap` registration order (direct before transitive).
27/// 3. Attach Maven/Gradle coordinates to nodes for provenance queries.
28///
29/// [`ClasspathNodeMetadata`]: sqry_core::graph::unified::storage::ClasspathNodeMetadata
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct ClasspathProvenance {
32    /// Absolute path to the JAR file.
33    pub jar_path: PathBuf,
34    /// Maven coordinates if known (e.g., `"com.google.guava:guava:33.0.0"`).
35    pub coordinates: Option<String>,
36    /// Conservative aggregate directness across all scopes.
37    ///
38    /// `true` means the JAR is direct in every recorded scope.
39    /// Mixed direct/transitive jars persist `false`; consult `scopes` for
40    /// precise per-module semantics.
41    pub is_direct: bool,
42    /// Module/root scopes where this JAR appears.
43    #[serde(default)]
44    pub scopes: Vec<ClasspathScope>,
45}
46
47impl ClasspathProvenance {
48    /// Returns whether the JAR is direct in at least one scope.
49    #[must_use]
50    pub fn has_direct_scope(&self) -> bool {
51        self.scopes.iter().any(|scope| scope.is_direct)
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn test_provenance_roundtrip_json() {
61        let prov = ClasspathProvenance {
62            jar_path: PathBuf::from("/home/user/.m2/repository/guava-33.0.0.jar"),
63            coordinates: Some("com.google.guava:guava:33.0.0".to_owned()),
64            is_direct: true,
65            scopes: vec![ClasspathScope {
66                module_name: "app".to_owned(),
67                module_root: PathBuf::from("/repo/app"),
68                is_direct: true,
69            }],
70        };
71
72        let json = serde_json::to_string(&prov).unwrap();
73        let deserialized: ClasspathProvenance = serde_json::from_str(&json).unwrap();
74        assert_eq!(deserialized.jar_path, prov.jar_path);
75        assert_eq!(deserialized.coordinates, prov.coordinates);
76        assert_eq!(deserialized.is_direct, prov.is_direct);
77    }
78
79    #[test]
80    fn test_provenance_transitive_no_coordinates() {
81        let prov = ClasspathProvenance {
82            jar_path: PathBuf::from("/tmp/some-transitive-1.0.jar"),
83            coordinates: None,
84            is_direct: false,
85            scopes: vec![ClasspathScope {
86                module_name: "lib".to_owned(),
87                module_root: PathBuf::from("/repo/lib"),
88                is_direct: false,
89            }],
90        };
91
92        assert!(!prov.is_direct);
93        assert!(prov.coordinates.is_none());
94    }
95
96    #[test]
97    fn test_provenance_postcard_roundtrip() {
98        let prov = ClasspathProvenance {
99            jar_path: PathBuf::from("/repo/.gradle/caches/guava-33.jar"),
100            coordinates: Some("com.google.guava:guava:33.0.0".to_owned()),
101            is_direct: false,
102            scopes: vec![ClasspathScope {
103                module_name: "app".to_owned(),
104                module_root: PathBuf::from("/repo/app"),
105                is_direct: false,
106            }],
107        };
108
109        let bytes = postcard::to_allocvec(&prov).unwrap();
110        let deserialized: ClasspathProvenance = postcard::from_bytes(&bytes).unwrap();
111        assert_eq!(deserialized.jar_path, prov.jar_path);
112        assert_eq!(deserialized.coordinates, prov.coordinates);
113        assert_eq!(deserialized.is_direct, prov.is_direct);
114    }
115}