Skip to main content

ldp_protocol/types/
provenance.rs

1//! LDP provenance tracking.
2//!
3//! Every LDP task result carries provenance metadata: who produced it,
4//! which model, what payload mode, confidence, and verification status.
5
6use crate::types::payload::PayloadMode;
7use crate::types::verification::{EvidenceRef, ProvenanceEntry, VerificationStatus};
8use serde::{Deserialize, Serialize};
9
10/// Provenance metadata attached to every LDP task result.
11///
12/// Embedded in the output `Value` so it flows through JamJet's existing
13/// pipeline without modification.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Provenance {
16    /// Delegate ID that produced this result.
17    pub produced_by: String,
18
19    /// Model version used.
20    pub model_version: String,
21
22    /// Payload mode used for this exchange.
23    pub payload_mode_used: PayloadMode,
24
25    /// Self-reported confidence (0.0 – 1.0).
26    pub confidence: Option<f64>,
27
28    /// Whether the result has been verified (e.g. by a second delegate).
29    #[deprecated(note = "Use verification_status instead")]
30    #[serde(default)]
31    pub verified: bool,
32
33    /// Session ID in which this result was produced.
34    pub session_id: Option<String>,
35
36    /// Timestamp of production.
37    pub timestamp: Option<String>,
38
39    /// Tokens consumed by the delegate (delegate-reported).
40    #[serde(skip_serializing_if = "Option::is_none", default)]
41    pub tokens_used: Option<u64>,
42
43    /// Cost incurred in USD (delegate-reported).
44    #[serde(skip_serializing_if = "Option::is_none", default)]
45    pub cost_usd: Option<f64>,
46
47    /// Contract ID this result was produced under.
48    #[serde(skip_serializing_if = "Option::is_none", default)]
49    pub contract_id: Option<String>,
50
51    /// Whether the contract was satisfied (set by client-side validation).
52    #[serde(skip_serializing_if = "Option::is_none", default)]
53    pub contract_satisfied: Option<bool>,
54
55    /// List of contract violation codes (set by client-side validation).
56    #[serde(default)]
57    pub contract_violations: Vec<String>,
58
59    /// Granular verification status.
60    #[serde(default)]
61    pub verification_status: VerificationStatus,
62
63    /// Supporting evidence references.
64    #[serde(default)]
65    pub evidence: Vec<EvidenceRef>,
66
67    /// Delegation lineage chain (newest hop first).
68    #[serde(default)]
69    pub lineage: Vec<ProvenanceEntry>,
70}
71
72impl Provenance {
73    /// Create a new provenance record.
74    #[allow(deprecated)]
75    pub fn new(delegate_id: impl Into<String>, model_version: impl Into<String>) -> Self {
76        Self {
77            produced_by: delegate_id.into(),
78            model_version: model_version.into(),
79            payload_mode_used: PayloadMode::SemanticFrame,
80            confidence: None,
81            verified: false,
82            session_id: None,
83            timestamp: Some(chrono::Utc::now().to_rfc3339()),
84            tokens_used: None,
85            cost_usd: None,
86            contract_id: None,
87            contract_satisfied: None,
88            contract_violations: Vec::new(),
89            verification_status: VerificationStatus::Unverified,
90            evidence: Vec::new(),
91            lineage: Vec::new(),
92        }
93    }
94
95    /// Sync verified bool with verification_status.
96    /// verification_status is authoritative.
97    pub fn normalize(&mut self) {
98        #[allow(deprecated)]
99        {
100            if self.verification_status == VerificationStatus::Unverified && self.verified {
101                self.verification_status = VerificationStatus::SelfVerified;
102            }
103            self.verified = self.verification_status != VerificationStatus::Unverified;
104        }
105    }
106
107    /// Convert to a JSON Value for embedding in task output.
108    pub fn to_value(&self) -> serde_json::Value {
109        serde_json::to_value(self).unwrap_or_default()
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn new_has_no_contract_fields() {
119        let p = Provenance::new("d1", "v1");
120        assert!(p.contract_id.is_none());
121        assert!(p.contract_satisfied.is_none());
122        assert!(p.contract_violations.is_empty());
123        assert!(p.tokens_used.is_none());
124        assert!(p.cost_usd.is_none());
125    }
126
127    #[test]
128    fn with_usage() {
129        let mut p = Provenance::new("d1", "v1");
130        p.tokens_used = Some(5000);
131        p.cost_usd = Some(0.03);
132        let json = serde_json::to_value(&p).unwrap();
133        let restored: Provenance = serde_json::from_value(json).unwrap();
134        assert_eq!(restored.tokens_used, Some(5000));
135        assert_eq!(restored.cost_usd, Some(0.03));
136    }
137
138    #[test]
139    fn backward_compat_deserialization() {
140        let old_json = serde_json::json!({
141            "produced_by": "d1",
142            "model_version": "v1",
143            "payload_mode_used": "text",
144            "verified": false
145        });
146        let p: Provenance = serde_json::from_value(old_json).unwrap();
147        assert_eq!(p.produced_by, "d1");
148        assert!(p.contract_id.is_none());
149        assert!(p.contract_violations.is_empty());
150    }
151
152    #[test]
153    fn provenance_new_has_unverified_status() {
154        let p = Provenance::new("d1", "v1");
155        assert_eq!(p.verification_status, VerificationStatus::Unverified);
156        assert!(p.evidence.is_empty());
157        assert!(p.lineage.is_empty());
158    }
159
160    #[test]
161    fn provenance_normalize_syncs_verified_to_status() {
162        let mut p = Provenance::new("d1", "v1");
163        p.verification_status = VerificationStatus::PeerVerified;
164        p.normalize();
165        #[allow(deprecated)]
166        {
167            assert!(p.verified);
168        }
169    }
170
171    #[test]
172    fn provenance_normalize_syncs_old_verified_true() {
173        let mut p = Provenance::new("d1", "v1");
174        #[allow(deprecated)]
175        {
176            p.verified = true;
177        }
178        p.normalize();
179        assert_eq!(p.verification_status, VerificationStatus::SelfVerified);
180    }
181
182    #[test]
183    fn provenance_backward_compat_no_verification_fields() {
184        let old_json = serde_json::json!({
185            "produced_by": "d1",
186            "model_version": "v1",
187            "payload_mode_used": "text",
188            "verified": true
189        });
190        let mut p: Provenance = serde_json::from_value(old_json).unwrap();
191        p.normalize();
192        assert_eq!(p.verification_status, VerificationStatus::SelfVerified);
193        assert!(p.evidence.is_empty());
194        assert!(p.lineage.is_empty());
195    }
196}