Skip to main content

urge_core/
decision.rs

1//! Verdict and audit-trail types.
2//!
3//! Every evaluation produces a [`Verdict`] that is fully self-describing:
4//! you can reconstruct *exactly* why the governance engine reached its conclusion
5//! by walking the [`LogicTrace`].
6
7use crate::engine::Paradigm;
8use crate::symbol::ParadigmSet;
9
10/// The outcome of a complete governance evaluation.
11///
12/// This is the output of the Figure 26 pipeline.
13#[derive(Debug, Clone)]
14// Serialize-only: holds `&'static str` fields for zero-alloc use, which cannot Deserialize.
15#[cfg_attr(feature = "serde", derive(serde::Serialize))]
16pub struct Verdict {
17    /// Whether the evaluated expression is valid / permitted / satisfied.
18    pub valid: bool,
19
20    /// Overall confidence score [0.0, 1.0].
21    ///
22    /// Derived from the agreement ratio across paradigm engines:
23    ///   1.0 = all active engines agree
24    ///   0.5 = split across paradigms
25    ///   0.0 = complete disagreement (paraconsistent scenario)
26    pub confidence: Confidence,
27
28    /// Which paradigms were active and evaluated.
29    pub paradigms_evaluated: ParadigmSet,
30
31    /// The full audit trail — every routing and evaluation decision.
32    pub trace: LogicTrace,
33
34    /// Cross-validation result from the meta-engine.
35    pub cross_validation: CrossValidation,
36
37    /// Formal notation of the evaluated expression (Unicode logic symbols).
38    #[cfg(feature = "alloc")]
39    pub formal_notation: alloc::string::String,
40
41    /// Policy citations — the authoritative sources that justify the verdict.
42    #[cfg(feature = "alloc")]
43    pub citations: alloc::vec::Vec<Citation>,
44}
45
46impl Verdict {
47    /// Construct a fast-path denial with minimal tracing.
48    /// Used for hard-coded prohibitions (e.g., in the BIOS access-control path).
49    pub fn deny_immediate(reason: &'static str) -> Self {
50        let mut trace = LogicTrace::new();
51        trace.push(TraceEntry {
52            stage: Stage::CrossValidation,
53            paradigm: Some(Paradigm::Deontic),
54            description: reason,
55            outcome: EntryOutcome::Denied,
56        });
57        Verdict {
58            valid: false,
59            confidence: Confidence::CERTAIN,
60            paradigms_evaluated: {
61                let mut s = ParadigmSet::empty();
62                s.insert(Paradigm::Deontic);
63                s
64            },
65            trace,
66            cross_validation: CrossValidation {
67                consistent: false,
68                conflicts_detected: 1,
69                conflict_detail: Some(reason),
70            },
71            #[cfg(feature = "alloc")]
72            formal_notation: alloc::format!("¬permitted({reason})"),
73            #[cfg(feature = "alloc")]
74            citations: alloc::vec![],
75        }
76    }
77
78    /// Construct a fast-path permit.
79    pub fn permit_immediate(reason: &'static str) -> Self {
80        let mut trace = LogicTrace::new();
81        trace.push(TraceEntry {
82            stage: Stage::CrossValidation,
83            paradigm: Some(Paradigm::Deontic),
84            description: reason,
85            outcome: EntryOutcome::Permitted,
86        });
87        Verdict {
88            valid: true,
89            confidence: Confidence::CERTAIN,
90            paradigms_evaluated: {
91                let mut s = ParadigmSet::empty();
92                s.insert(Paradigm::Deontic);
93                s
94            },
95            trace,
96            cross_validation: CrossValidation {
97                consistent: true,
98                conflicts_detected: 0,
99                conflict_detail: None,
100            },
101            #[cfg(feature = "alloc")]
102            formal_notation: alloc::format!("permitted({reason})"),
103            #[cfg(feature = "alloc")]
104            citations: alloc::vec![],
105        }
106    }
107}
108
109// ── Confidence ─────────────────────────────────────────────────────────────────
110
111/// A confidence score in the range [0, 255] mapped to [0.0, 1.0].
112/// Stored as u8 to avoid float in no_std environments where soft-float is slow.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
114#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
115pub struct Confidence(pub u8);
116
117impl Confidence {
118    pub const CERTAIN: Confidence = Confidence(255);
119    pub const HIGH: Confidence = Confidence(204); // ~0.80
120    pub const MEDIUM: Confidence = Confidence(153); // ~0.60
121    pub const LOW: Confidence = Confidence(102); // ~0.40
122    pub const UNCERTAIN: Confidence = Confidence(51); // ~0.20
123    pub const NONE: Confidence = Confidence(0);
124
125    /// Compute from agreement ratio: `agreed` engines out of `total`.
126    pub fn from_agreement(agreed: u8, total: u8) -> Self {
127        if total == 0 {
128            return Confidence::NONE;
129        }
130        Confidence(((agreed as u16 * 255) / total as u16) as u8)
131    }
132
133    pub fn as_f32(self) -> f32 {
134        self.0 as f32 / 255.0
135    }
136}
137
138// ── CrossValidation ─────────────────────────────────────────────────────────────
139
140/// Result of the cross-system validation stage (Step 3 of Figure 26).
141///
142/// This is the key differentiation: the meta-engine checks that results from
143/// multiple paradigm engines are logically consistent with each other before
144/// producing a final verdict.
145#[derive(Debug, Clone)]
146// Serialize-only: holds `&'static str` fields for zero-alloc use, which cannot Deserialize.
147#[cfg_attr(feature = "serde", derive(serde::Serialize))]
148pub struct CrossValidation {
149    /// True if all paradigm engines reached compatible conclusions.
150    pub consistent: bool,
151    /// Number of inter-paradigm conflicts detected.
152    pub conflicts_detected: u8,
153    /// Description of the first conflict found, if any.
154    pub conflict_detail: Option<&'static str>,
155}
156
157impl CrossValidation {
158    pub fn ok() -> Self {
159        CrossValidation {
160            consistent: true,
161            conflicts_detected: 0,
162            conflict_detail: None,
163        }
164    }
165}
166
167// ── LogicTrace ─────────────────────────────────────────────────────────────────
168
169/// The complete audit trail for one governance evaluation.
170///
171/// Bounded to 64 entries on embedded targets. On `std` targets this can grow.
172#[derive(Debug, Clone)]
173// Serialize-only: holds `&'static str` fields (via TraceEntry) for zero-alloc use, which cannot Deserialize.
174#[cfg_attr(feature = "serde", derive(serde::Serialize))]
175pub struct LogicTrace {
176    #[cfg(feature = "alloc")]
177    pub entries: alloc::vec::Vec<TraceEntry>,
178    #[cfg(not(feature = "alloc"))]
179    pub entries: heapless::Vec<TraceEntry, 64>,
180}
181
182impl LogicTrace {
183    pub fn new() -> Self {
184        LogicTrace {
185            #[cfg(feature = "alloc")]
186            entries: alloc::vec::Vec::new(),
187            #[cfg(not(feature = "alloc"))]
188            entries: heapless::Vec::new(),
189        }
190    }
191
192    pub fn push(&mut self, entry: TraceEntry) {
193        #[cfg(feature = "alloc")]
194        self.entries.push(entry);
195        #[cfg(not(feature = "alloc"))]
196        let _ = self.entries.push(entry);
197    }
198
199    pub fn len(&self) -> usize {
200        self.entries.len()
201    }
202
203    pub fn is_empty(&self) -> bool {
204        self.entries.is_empty()
205    }
206}
207
208impl Default for LogicTrace {
209    fn default() -> Self {
210        Self::new()
211    }
212}
213
214/// A single step in the logic trace.
215#[derive(Debug, Clone)]
216// Serialize-only: `description` is `&'static str` for zero-alloc use, which cannot Deserialize.
217#[cfg_attr(feature = "serde", derive(serde::Serialize))]
218pub struct TraceEntry {
219    /// Which pipeline stage produced this entry.
220    pub stage: Stage,
221    /// The paradigm involved, if applicable.
222    pub paradigm: Option<Paradigm>,
223    /// Human-readable description (static string for zero-allocation on embedded).
224    pub description: &'static str,
225    /// The outcome of this step.
226    pub outcome: EntryOutcome,
227}
228
229/// Pipeline stages (maps to the Figure 26 workflow steps).
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
232pub enum Stage {
233    Tokenization,
234    ParadigmDetection,
235    AstConstruction,
236    EngineRouting,
237    EngineEvaluation,
238    CrossValidation,
239    VerdictSynthesis,
240}
241
242/// Outcome of a trace step.
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
245pub enum EntryOutcome {
246    Permitted,
247    Denied,
248    Evaluated,
249    Routed,
250    Conflict,
251    Skipped,
252}
253
254/// A regulatory or policy citation anchoring a governance decision.
255#[cfg(feature = "alloc")]
256#[derive(Debug, Clone)]
257#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
258pub struct Citation {
259    /// Short identifier, e.g. "HIPAA-§164.312(a)"
260    pub id: alloc::string::String,
261    /// Human-readable description of the cited requirement.
262    pub description: alloc::string::String,
263}