use serde::{Deserialize, Serialize};
use std::time::SystemTime;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvenanceEnvelope {
pub input_hash: String,
#[serde(with = "system_time_serde")]
pub submitted_at: SystemTime,
pub source_system: String,
pub submitted_by: String,
pub correlation_id: String,
pub metadata: serde_json::Value,
}
impl Default for ProvenanceEnvelope {
fn default() -> Self {
Self {
input_hash: String::new(),
submitted_at: SystemTime::now(),
source_system: String::new(),
submitted_by: String::new(),
correlation_id: String::new(),
metadata: serde_json::Value::Null,
}
}
}
impl ProvenanceEnvelope {
pub fn new(source_system: impl Into<String>, submitted_by: impl Into<String>) -> Self {
Self {
source_system: source_system.into(),
submitted_by: submitted_by.into(),
..Default::default()
}
}
pub fn with_correlation_id(mut self, id: impl Into<String>) -> Self {
self.correlation_id = id.into();
self
}
pub fn with_input_hash(mut self, hash: impl Into<String>) -> Self {
self.input_hash = hash.into();
self
}
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = metadata;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelTraceLink {
pub trace_id: String,
pub mode: TraceMode,
pub location: String,
}
impl Default for KernelTraceLink {
fn default() -> Self {
Self {
trace_id: String::new(),
mode: TraceMode::AuditOnly,
location: String::new(),
}
}
}
impl KernelTraceLink {
pub fn audit_only(trace_id: impl Into<String>) -> Self {
Self {
trace_id: trace_id.into(),
mode: TraceMode::AuditOnly,
location: String::new(),
}
}
pub fn replayable(trace_id: impl Into<String>, location: impl Into<String>) -> Self {
Self {
trace_id: trace_id.into(),
mode: TraceMode::Replayable,
location: location.into(),
}
}
pub fn is_replayable(&self) -> bool {
self.mode == TraceMode::Replayable
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TraceMode {
Replayable,
AuditOnly,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplayEnvelope {
pub input_hash: String,
pub output_hash: String,
pub seed: u64,
pub solver_version: String,
pub pack_version: String,
pub library_version: String,
}
impl ReplayEnvelope {
pub fn new(
input_hash: impl Into<String>,
output_hash: impl Into<String>,
seed: u64,
solver_version: impl Into<String>,
pack_version: impl Into<String>,
) -> Self {
Self {
input_hash: input_hash.into(),
output_hash: output_hash.into(),
seed,
solver_version: solver_version.into(),
pack_version: pack_version.into(),
library_version: env!("CARGO_PKG_VERSION").to_string(),
}
}
pub fn minimal(seed: u64) -> Self {
Self {
input_hash: String::new(),
output_hash: String::new(),
seed,
solver_version: "test".to_string(),
pack_version: "test".to_string(),
library_version: env!("CARGO_PKG_VERSION").to_string(),
}
}
}
mod system_time_serde {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub fn serialize<S>(time: &SystemTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let duration = time.duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO);
duration.as_secs_f64().serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<SystemTime, D::Error>
where
D: Deserializer<'de>,
{
let secs = f64::deserialize(deserializer)?;
Ok(UNIX_EPOCH + Duration::from_secs_f64(secs))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_provenance_builder() {
let prov = ProvenanceEnvelope::new("test-system", "test-user")
.with_correlation_id("corr-123")
.with_input_hash("sha256:abc");
assert_eq!(prov.source_system, "test-system");
assert_eq!(prov.submitted_by, "test-user");
assert_eq!(prov.correlation_id, "corr-123");
assert_eq!(prov.input_hash, "sha256:abc");
}
#[test]
fn test_trace_link() {
let audit = KernelTraceLink::audit_only("trace-001");
assert!(!audit.is_replayable());
let replay = KernelTraceLink::replayable("trace-002", "/traces/002.json");
assert!(replay.is_replayable());
assert_eq!(replay.location, "/traces/002.json");
}
#[test]
fn test_replay_envelope() {
let envelope = ReplayEnvelope::new("input-hash", "output-hash", 42, "greedy-v1", "1.0.0");
assert_eq!(envelope.seed, 42);
assert_eq!(envelope.solver_version, "greedy-v1");
}
#[test]
fn test_serde_roundtrip() {
let prov = ProvenanceEnvelope::new("system", "user");
let json = serde_json::to_string(&prov).unwrap();
let restored: ProvenanceEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(restored.source_system, prov.source_system);
}
}