Skip to main content

converge_optimization/gate/
provenance.rs

1//! Provenance information for audit trail
2
3use serde::{Deserialize, Serialize};
4use std::time::SystemTime;
5
6/// Provenance information for audit trail
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ProvenanceEnvelope {
9    /// Input data hash (for integrity verification)
10    pub input_hash: String,
11    /// Timestamp when problem was submitted
12    #[serde(with = "system_time_serde")]
13    pub submitted_at: SystemTime,
14    /// Source system that created this problem
15    pub source_system: String,
16    /// User or service that submitted
17    pub submitted_by: String,
18    /// Correlation ID for distributed tracing
19    pub correlation_id: String,
20    /// Additional metadata
21    pub metadata: serde_json::Value,
22}
23
24impl Default for ProvenanceEnvelope {
25    fn default() -> Self {
26        Self {
27            input_hash: String::new(),
28            submitted_at: SystemTime::now(),
29            source_system: String::new(),
30            submitted_by: String::new(),
31            correlation_id: String::new(),
32            metadata: serde_json::Value::Null,
33        }
34    }
35}
36
37impl ProvenanceEnvelope {
38    /// Create with minimal info
39    pub fn new(source_system: impl Into<String>, submitted_by: impl Into<String>) -> Self {
40        Self {
41            source_system: source_system.into(),
42            submitted_by: submitted_by.into(),
43            ..Default::default()
44        }
45    }
46
47    /// Set correlation ID
48    pub fn with_correlation_id(mut self, id: impl Into<String>) -> Self {
49        self.correlation_id = id.into();
50        self
51    }
52
53    /// Set input hash
54    pub fn with_input_hash(mut self, hash: impl Into<String>) -> Self {
55        self.input_hash = hash.into();
56        self
57    }
58
59    /// Set metadata
60    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
61        self.metadata = metadata;
62        self
63    }
64}
65
66/// Link to kernel trace for replay/audit
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct KernelTraceLink {
69    /// Trace ID
70    pub trace_id: String,
71    /// Whether this is for replay or audit-only
72    pub mode: TraceMode,
73    /// URL or path to trace data
74    pub location: String,
75}
76
77impl Default for KernelTraceLink {
78    fn default() -> Self {
79        Self {
80            trace_id: String::new(),
81            mode: TraceMode::AuditOnly,
82            location: String::new(),
83        }
84    }
85}
86
87impl KernelTraceLink {
88    /// Create audit-only trace link
89    pub fn audit_only(trace_id: impl Into<String>) -> Self {
90        Self {
91            trace_id: trace_id.into(),
92            mode: TraceMode::AuditOnly,
93            location: String::new(),
94        }
95    }
96
97    /// Create replayable trace link
98    pub fn replayable(trace_id: impl Into<String>, location: impl Into<String>) -> Self {
99        Self {
100            trace_id: trace_id.into(),
101            mode: TraceMode::Replayable,
102            location: location.into(),
103        }
104    }
105
106    /// Check if this trace supports replay
107    pub fn is_replayable(&self) -> bool {
108        self.mode == TraceMode::Replayable
109    }
110}
111
112/// Trace mode
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "snake_case")]
115pub enum TraceMode {
116    /// Full replay capability
117    Replayable,
118    /// Audit/logging only
119    AuditOnly,
120}
121
122/// Replay envelope for solver reproducibility
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ReplayEnvelope {
125    /// Hash of input data
126    pub input_hash: String,
127    /// Hash of output data
128    pub output_hash: String,
129    /// Seed used for this run
130    pub seed: u64,
131    /// Solver version
132    pub solver_version: String,
133    /// Pack version
134    pub pack_version: String,
135    /// Library version
136    pub library_version: String,
137}
138
139impl ReplayEnvelope {
140    /// Create for current library version
141    pub fn new(
142        input_hash: impl Into<String>,
143        output_hash: impl Into<String>,
144        seed: u64,
145        solver_version: impl Into<String>,
146        pack_version: impl Into<String>,
147    ) -> Self {
148        Self {
149            input_hash: input_hash.into(),
150            output_hash: output_hash.into(),
151            seed,
152            solver_version: solver_version.into(),
153            pack_version: pack_version.into(),
154            library_version: env!("CARGO_PKG_VERSION").to_string(),
155        }
156    }
157
158    /// Create a minimal replay envelope for testing
159    pub fn minimal(seed: u64) -> Self {
160        Self {
161            input_hash: String::new(),
162            output_hash: String::new(),
163            seed,
164            solver_version: "test".to_string(),
165            pack_version: "test".to_string(),
166            library_version: env!("CARGO_PKG_VERSION").to_string(),
167        }
168    }
169}
170
171/// Serde support for SystemTime as unix timestamp
172mod system_time_serde {
173    use serde::{Deserialize, Deserializer, Serialize, Serializer};
174    use std::time::{Duration, SystemTime, UNIX_EPOCH};
175
176    pub fn serialize<S>(time: &SystemTime, serializer: S) -> Result<S::Ok, S::Error>
177    where
178        S: Serializer,
179    {
180        let duration = time.duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO);
181        duration.as_secs_f64().serialize(serializer)
182    }
183
184    pub fn deserialize<'de, D>(deserializer: D) -> Result<SystemTime, D::Error>
185    where
186        D: Deserializer<'de>,
187    {
188        let secs = f64::deserialize(deserializer)?;
189        Ok(UNIX_EPOCH + Duration::from_secs_f64(secs))
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_provenance_builder() {
199        let prov = ProvenanceEnvelope::new("test-system", "test-user")
200            .with_correlation_id("corr-123")
201            .with_input_hash("sha256:abc");
202
203        assert_eq!(prov.source_system, "test-system");
204        assert_eq!(prov.submitted_by, "test-user");
205        assert_eq!(prov.correlation_id, "corr-123");
206        assert_eq!(prov.input_hash, "sha256:abc");
207    }
208
209    #[test]
210    fn test_trace_link() {
211        let audit = KernelTraceLink::audit_only("trace-001");
212        assert!(!audit.is_replayable());
213
214        let replay = KernelTraceLink::replayable("trace-002", "/traces/002.json");
215        assert!(replay.is_replayable());
216        assert_eq!(replay.location, "/traces/002.json");
217    }
218
219    #[test]
220    fn test_replay_envelope() {
221        let envelope = ReplayEnvelope::new("input-hash", "output-hash", 42, "greedy-v1", "1.0.0");
222        assert_eq!(envelope.seed, 42);
223        assert_eq!(envelope.solver_version, "greedy-v1");
224    }
225
226    #[test]
227    fn test_serde_roundtrip() {
228        let prov = ProvenanceEnvelope::new("system", "user");
229        let json = serde_json::to_string(&prov).unwrap();
230        let restored: ProvenanceEnvelope = serde_json::from_str(&json).unwrap();
231        assert_eq!(restored.source_system, prov.source_system);
232    }
233}