Skip to main content

urge_meta/
pipeline.rs

1//! The complete Figure 26 pipeline as a single callable unit.
2//!
3//! `GovernancePipeline::evaluate()` drives all seven stages in sequence and
4//! returns a final `Verdict`. This is the public entry point for all governance
5//! decisions — whether on a BIOS chip or a healthcare ERP agentic layer.
6//!
7//! ## Timing
8//!
9//! Target: <50ms for typical governance decisions (10-50 active rules).
10//! Actual observed performance in the shell proof: ~1ms for simple rule chains.
11//! Rust implementation: expected <100µs on x86, <5ms on ARM Cortex-M4.
12
13use urge_core::{
14    ast::AstNode,
15    decision::{EntryOutcome, LogicTrace, Stage, TraceEntry, Verdict},
16    engine::{EvalContext, Paradigm},
17    symbol::{ParadigmSet, UnicodeSemanticDictionary},
18};
19
20use crate::{
21    parser::Parser, router::EngineRouter, tokenizer::Tokenizer, validator::CrossValidator,
22};
23
24#[cfg(feature = "alloc")]
25use alloc::string::String;
26
27/// Configuration for the governance pipeline.
28#[derive(Debug, Clone)]
29pub struct PipelineConfig {
30    /// Maximum AST recursion depth. Limits resource consumption on embedded.
31    pub depth_limit: u8,
32    /// If true, always run all engines regardless of paradigm detection.
33    /// Useful for audit/compliance scenarios where every paradigm must certify.
34    pub exhaustive_evaluation: bool,
35    /// Minimum confidence threshold. Verdicts below this threshold are denied.
36    pub confidence_threshold: u8,
37}
38
39impl Default for PipelineConfig {
40    fn default() -> Self {
41        PipelineConfig {
42            depth_limit: 32,
43            exhaustive_evaluation: false,
44            confidence_threshold: 128, // ~0.50
45        }
46    }
47}
48
49impl PipelineConfig {
50    /// Conservative config for healthcare/HIPAA contexts:
51    /// exhaustive evaluation, higher confidence threshold.
52    pub fn healthcare() -> Self {
53        PipelineConfig {
54            depth_limit: 16,
55            exhaustive_evaluation: true,
56            confidence_threshold: 204, // ~0.80
57        }
58    }
59
60    /// Minimal config for embedded BIOS-like contexts:
61    /// fast, shallow evaluation.
62    pub fn embedded() -> Self {
63        PipelineConfig {
64            depth_limit: 8,
65            exhaustive_evaluation: false,
66            confidence_threshold: 51, // ~0.20
67        }
68    }
69}
70
71/// The complete governance pipeline (Figure 26).
72pub struct GovernancePipeline {
73    pub config: PipelineConfig,
74    tokenizer: Tokenizer,
75}
76
77impl GovernancePipeline {
78    pub fn new(config: PipelineConfig) -> Self {
79        GovernancePipeline {
80            config,
81            tokenizer: Tokenizer::new(),
82        }
83    }
84
85    pub fn default_healthcare() -> Self {
86        Self::new(PipelineConfig::healthcare())
87    }
88
89    pub fn default_embedded() -> Self {
90        Self::new(PipelineConfig::embedded())
91    }
92
93    /// Evaluate a governance expression given as a string.
94    ///
95    /// Full pipeline: tokenize → detect → parse → route → evaluate → validate → synthesize.
96    ///
97    /// This is the primary API for string-based governance expressions.
98    #[cfg(feature = "alloc")]
99    pub fn evaluate_str(&self, expression: &str, ctx: &EvalContext<'_>) -> Verdict {
100        let mut trace = LogicTrace::new();
101
102        // ── Stage 1: Tokenization ──────────────────────────────────────────
103        trace.push(TraceEntry {
104            stage: Stage::Tokenization,
105            paradigm: None,
106            description: "tokenizing input",
107            outcome: EntryOutcome::Evaluated,
108        });
109        let tokens = self.tokenizer.tokenize(expression);
110
111        if tokens.is_empty() {
112            return Verdict::deny_immediate("empty or unrecognized expression");
113        }
114
115        // ── Stage 2: Paradigm Detection ────────────────────────────────────
116        trace.push(TraceEntry {
117            stage: Stage::ParadigmDetection,
118            paradigm: None,
119            description: "detecting active paradigms",
120            outcome: EntryOutcome::Evaluated,
121        });
122        let classes: alloc::vec::Vec<_> = tokens.iter().map(|t| t.class).collect();
123        let active_paradigms = if self.config.exhaustive_evaluation {
124            // All paradigms active in exhaustive mode.
125            let mut all = ParadigmSet::empty();
126            for &p in Paradigm::ALL {
127                all.insert(p);
128            }
129            all
130        } else {
131            UnicodeSemanticDictionary::detect_paradigms(&classes)
132        };
133
134        for p in active_paradigms.iter() {
135            trace.push(TraceEntry {
136                stage: Stage::ParadigmDetection,
137                paradigm: Some(p),
138                description: p.name(),
139                outcome: EntryOutcome::Evaluated,
140            });
141        }
142
143        // ── Stage 3: AST Construction ──────────────────────────────────────
144        trace.push(TraceEntry {
145            stage: Stage::AstConstruction,
146            paradigm: None,
147            description: "building AST",
148            outcome: EntryOutcome::Evaluated,
149        });
150        let mut parser = Parser::new(tokens);
151        let ast = match parser.parse() {
152            Some(a) => a,
153            None => return Verdict::deny_immediate("failed to parse expression"),
154        };
155
156        self.evaluate_ast(&ast, active_paradigms, ctx, trace)
157    }
158
159    /// Evaluate a pre-built AST directly.
160    ///
161    /// Use this when you construct governance expressions programmatically
162    /// (e.g., from a rule database) to skip tokenization and parsing overhead.
163    pub fn evaluate_ast(
164        &self,
165        ast: &AstNode,
166        active_paradigms: ParadigmSet,
167        ctx: &EvalContext<'_>,
168        mut trace: LogicTrace,
169    ) -> Verdict {
170        // ── Stage 4 + 5: Engine Routing and Evaluation ────────────────────
171        trace.push(TraceEntry {
172            stage: Stage::EngineRouting,
173            paradigm: None,
174            description: "routing to engines",
175            outcome: EntryOutcome::Evaluated,
176        });
177
178        #[cfg(feature = "alloc")]
179        let verdicts = EngineRouter::route(ast, active_paradigms, ctx, &mut trace);
180
181        #[cfg(not(feature = "alloc"))]
182        let single_verdict = EngineRouter::route_single(ast, active_paradigms, ctx, &mut trace);
183
184        // ── Stage 6: Cross-System Validation ──────────────────────────────
185        trace.push(TraceEntry {
186            stage: Stage::CrossValidation,
187            paradigm: None,
188            description: "cross-system validation",
189            outcome: EntryOutcome::Evaluated,
190        });
191
192        #[cfg(feature = "alloc")]
193        let (final_valid, confidence, cross_validation) =
194            CrossValidator::validate(&verdicts, &mut trace);
195
196        #[cfg(not(feature = "alloc"))]
197        let (final_valid, confidence, cross_validation) = match &single_verdict {
198            Ok(v) => {
199                let cv = CrossValidator::validate_single(v, &mut trace);
200                (v.valid, v.confidence, cv)
201            }
202            Err(_) => (
203                false,
204                Confidence::NONE,
205                CrossValidation {
206                    consistent: false,
207                    conflicts_detected: 1,
208                    conflict_detail: Some("engine error"),
209                },
210            ),
211        };
212
213        // Apply confidence threshold.
214        let threshold_met = confidence.0 >= self.config.confidence_threshold;
215
216        // ── Stage 7: Verdict Synthesis ─────────────────────────────────────
217        trace.push(TraceEntry {
218            stage: Stage::VerdictSynthesis,
219            paradigm: None,
220            description: if threshold_met {
221                "confidence threshold met"
222            } else {
223                "confidence threshold not met — deny"
224            },
225            outcome: if final_valid && threshold_met {
226                EntryOutcome::Permitted
227            } else {
228                EntryOutcome::Denied
229            },
230        });
231
232        Verdict {
233            valid: final_valid && threshold_met,
234            confidence,
235            paradigms_evaluated: active_paradigms,
236            trace,
237            cross_validation,
238            #[cfg(feature = "alloc")]
239            formal_notation: synthesize_notation(&verdicts),
240            #[cfg(feature = "alloc")]
241            citations: collect_citations(&verdicts),
242        }
243    }
244}
245
246#[cfg(feature = "alloc")]
247fn synthesize_notation(verdicts: &[Result<Verdict, urge_core::engine::EngineError>]) -> String {
248    let notations: alloc::vec::Vec<String> = verdicts
249        .iter()
250        .filter_map(|v| v.as_ref().ok())
251        .map(|v| v.formal_notation.clone())
252        .filter(|s| !s.is_empty())
253        .collect();
254    if notations.is_empty() {
255        String::from("(no notation)")
256    } else {
257        notations.join(" ∧ ")
258    }
259}
260
261#[cfg(feature = "alloc")]
262fn collect_citations(
263    verdicts: &[Result<Verdict, urge_core::engine::EngineError>],
264) -> alloc::vec::Vec<urge_core::decision::Citation> {
265    verdicts
266        .iter()
267        .filter_map(|v| v.as_ref().ok())
268        .flat_map(|v| v.citations.iter().cloned())
269        .collect()
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use urge_core::engine::{ContextValue, EvalContext};
276
277    fn empty_ctx() -> EvalContext<'static> {
278        EvalContext {
279            slots: &[],
280            logical_time: 0,
281            depth_limit: 16,
282        }
283    }
284
285    #[test]
286    #[cfg(feature = "alloc")]
287    fn evaluate_simple_true() {
288        let pipeline = GovernancePipeline::new(PipelineConfig::default());
289        let ctx = empty_ctx();
290        let verdict = pipeline.evaluate_str("true", &ctx);
291        assert!(verdict.valid);
292        assert!(verdict.confidence.0 > 0);
293    }
294
295    #[test]
296    #[cfg(feature = "alloc")]
297    fn evaluate_simple_false() {
298        let pipeline = GovernancePipeline::new(PipelineConfig::default());
299        let ctx = empty_ctx();
300        let verdict = pipeline.evaluate_str("false", &ctx);
301        assert!(!verdict.valid);
302    }
303
304    #[test]
305    #[cfg(feature = "alloc")]
306    fn evaluate_negation() {
307        let pipeline = GovernancePipeline::new(PipelineConfig::default());
308        let ctx = empty_ctx();
309        let verdict = pipeline.evaluate_str("not false", &ctx);
310        assert!(verdict.valid);
311    }
312
313    #[test]
314    #[cfg(feature = "alloc")]
315    fn evaluate_deontic_obligation() {
316        let pipeline = GovernancePipeline::new(PipelineConfig::default());
317        let slots = &[("consent:done", ContextValue::Bool(true))];
318        let ctx = EvalContext {
319            slots,
320            logical_time: 0,
321            depth_limit: 16,
322        };
323        // "must" keyword triggers deontic engine
324        let verdict = pipeline.evaluate_str("must true", &ctx);
325        // Deontic engine evaluates the inner expression.
326        assert!(verdict.paradigms_evaluated.contains(Paradigm::Deontic));
327    }
328
329    #[test]
330    #[cfg(feature = "alloc")]
331    fn healthcare_config_exhaustive() {
332        let pipeline = GovernancePipeline::default_healthcare();
333        assert!(pipeline.config.exhaustive_evaluation);
334        assert!(pipeline.config.confidence_threshold > 128);
335    }
336
337    #[test]
338    #[cfg(feature = "alloc")]
339    fn trace_is_non_empty() {
340        let pipeline = GovernancePipeline::new(PipelineConfig::default());
341        let ctx = empty_ctx();
342        let verdict = pipeline.evaluate_str("always true", &ctx);
343        assert!(!verdict.trace.is_empty());
344    }
345}