Skip to main content

perspt_sdk/
certificate.rs

1//! Residual certificate (PSP-8 System 2 / Gate B).
2//!
3//! When the system cannot reach stability it terminates with a residual
4//! certificate naming the remaining residuals, verifier routes, budget state,
5//! and ledger head — an honest stop rather than a success claim. A residual
6//! certificate is a first-class outcome, not a discarded failure.
7
8use serde::{Deserialize, Serialize};
9
10use crate::residual::{CorrectionDirection, IndependenceRoute, ResidualEvent};
11
12/// A budget that was exhausted, named in a certificate.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct BudgetRef {
15    pub name: String,
16    pub limit: u64,
17    pub used: u64,
18}
19
20/// A residual certificate (PSP-8 `ResidualCertificate`).
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct ResidualCertificate {
23    pub certificate_id: String,
24    pub node_id: String,
25    pub generation: u32,
26    /// Ledger head at the moment the certificate was issued.
27    pub ledger_head: String,
28    /// Final total energy `V`.
29    pub final_energy: f64,
30    /// Final residual vector.
31    pub final_residuals: Vec<ResidualEvent>,
32    /// Budgets that were exhausted.
33    pub exhausted_budgets: Vec<BudgetRef>,
34    /// Verifier independence routes exercised.
35    pub verifier_routes: Vec<IndependenceRoute>,
36    /// Identifiers of rejected (observed-only) attempts.
37    pub rejected_attempts: Vec<String>,
38    /// Correction directions that remain to be tried.
39    pub next_correction_directions: Vec<CorrectionDirection>,
40}
41
42impl ResidualCertificate {
43    /// Build a certificate from the final residual vector, deriving the verifier
44    /// routes and outstanding correction directions from the residuals.
45    pub fn from_residuals(
46        node_id: impl Into<String>,
47        generation: u32,
48        ledger_head: impl Into<String>,
49        final_energy: f64,
50        final_residuals: Vec<ResidualEvent>,
51    ) -> Self {
52        let mut verifier_routes: Vec<IndependenceRoute> = Vec::new();
53        let mut next_correction_directions: Vec<CorrectionDirection> = Vec::new();
54        for r in &final_residuals {
55            if !verifier_routes.contains(&r.sensor.route) {
56                verifier_routes.push(r.sensor.route);
57            }
58            next_correction_directions.extend(r.correction_directions.iter().cloned());
59        }
60        Self {
61            certificate_id: uuid::Uuid::new_v4().to_string(),
62            node_id: node_id.into(),
63            generation,
64            ledger_head: ledger_head.into(),
65            final_energy,
66            final_residuals,
67            exhausted_budgets: Vec::new(),
68            verifier_routes,
69            rejected_attempts: Vec::new(),
70            next_correction_directions,
71        }
72    }
73
74    pub fn with_exhausted_budget(mut self, budget: BudgetRef) -> Self {
75        self.exhausted_budgets.push(budget);
76        self
77    }
78
79    pub fn with_rejected_attempts(mut self, attempts: Vec<String>) -> Self {
80        self.rejected_attempts = attempts;
81        self
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::residual::{ResidualClass, ResidualSeverity, SensorRef};
89
90    #[test]
91    fn certificate_derives_routes_and_directions() {
92        let residual = ResidualEvent::new(
93            "n1",
94            2,
95            ResidualClass::ImportGraph,
96            ResidualSeverity::Error,
97            1.0,
98            SensorRef::new("rust-analyzer", IndependenceRoute::Lsp),
99        )
100        .unwrap()
101        .with_correction(CorrectionDirection::new(
102            ResidualClass::ImportGraph,
103            "add `use crate::foo::Bar;`",
104        ));
105
106        let cert = ResidualCertificate::from_residuals("n1", 2, "head-abc", 1.0, vec![residual])
107            .with_exhausted_budget(BudgetRef {
108                name: "correction".into(),
109                limit: 4,
110                used: 4,
111            });
112
113        assert_eq!(cert.verifier_routes, vec![IndependenceRoute::Lsp]);
114        assert_eq!(cert.next_correction_directions.len(), 1);
115        assert_eq!(cert.exhausted_budgets.len(), 1);
116        assert_eq!(cert.node_id, "n1");
117    }
118}