Skip to main content

u280_hardware_promoter/
result.rs

1//! Terminal result envelopes with all-or-nothing success claims.
2
3use crate::canonical::canonical_line;
4use crate::{FailureCode, PromotionError, PromotionResult, PromotionTransactionIdentity};
5use serde::Serialize;
6
7const FAILURE_KIND: &str = "hashsigs-u280-hardware-promotion-failure-v1";
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
10struct TerminalClaims {
11    hardware_execution: bool,
12    card_validated: bool,
13    cryptographic_outputs_valid: bool,
14    performance_validated: bool,
15    hardware_completion_promotable: bool,
16}
17
18impl TerminalClaims {
19    const fn failure() -> Self {
20        Self {
21            hardware_execution: false,
22            card_validated: false,
23            cryptographic_outputs_valid: false,
24            performance_validated: false,
25            hardware_completion_promotable: false,
26        }
27    }
28}
29
30/// Sealed nonterminal failure record. Its five terminal fields are always
31/// false and cannot be set by the caller.
32#[derive(Clone, Debug, Serialize)]
33pub struct PromotionFailure {
34    schema: u64,
35    kind: &'static str,
36    transaction: PromotionTransactionIdentity,
37    failure_code: FailureCode,
38    error: String,
39    #[serde(flatten)]
40    terminal: TerminalClaims,
41}
42
43impl PromotionFailure {
44    /// Converts an internal failure to a nonpromotable canonical envelope.
45    #[must_use]
46    pub fn new(transaction: &PromotionTransactionIdentity, error: &PromotionError) -> Self {
47        Self {
48            schema: 1,
49            kind: FAILURE_KIND,
50            transaction: transaction.clone(),
51            failure_code: error.code(),
52            error: error.message().to_owned(),
53            terminal: TerminalClaims::failure(),
54        }
55    }
56}
57
58/// The only document shape accepted by atomic publication in this source
59/// slice.
60///
61/// The legacy schema-1 request is deliberately incapable of producing a
62/// success variant. A future success-schema-2 type remains deferred until it
63/// can structurally require the complete route, oracle, card, clock,
64/// coordination, performance, and journal join. No partial success DTO is
65/// exposed in the meantime.
66#[derive(Clone, Debug)]
67pub enum PromotionDocument {
68    /// Explicitly nonpromotable failure evidence.
69    Failure(PromotionFailure),
70}
71
72impl PromotionDocument {
73    /// Typed identity that must equal the durable state burn before rename.
74    #[must_use]
75    pub const fn transaction_identity(&self) -> &PromotionTransactionIdentity {
76        match self {
77            Self::Failure(failure) => &failure.transaction,
78        }
79    }
80
81    /// Exact compact newline-terminated JSON bytes.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if serialization unexpectedly fails.
86    pub fn canonical_bytes(&self) -> PromotionResult<Vec<u8>> {
87        match self {
88            Self::Failure(failure) => canonical_line(failure),
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::{FAILURE_KIND, PromotionDocument, PromotionFailure};
96    use crate::journal::synthetic_identity;
97    use crate::{FailureCode, PromotionError};
98
99    #[test]
100    fn failure_serialization_has_five_false_terminal_claims() {
101        let error = PromotionError::new(FailureCode::RouteUnaccepted, "probe required");
102        let transaction = synthetic_identity(1).transaction_identity();
103        let document = PromotionDocument::Failure(PromotionFailure::new(&transaction, &error));
104        let value: serde_json::Value =
105            serde_json::from_slice(&document.canonical_bytes().unwrap()).unwrap();
106        for key in [
107            "hardware_execution",
108            "card_validated",
109            "cryptographic_outputs_valid",
110            "performance_validated",
111            "hardware_completion_promotable",
112        ] {
113            assert_eq!(value[key], false, "{key} was not false");
114        }
115        assert_eq!(value["kind"], FAILURE_KIND);
116        assert_eq!(value["failure_code"], "ROUTE_UNACCEPTED");
117    }
118}