1use 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#[derive(Debug, Clone)]
29pub struct PipelineConfig {
30 pub depth_limit: u8,
32 pub exhaustive_evaluation: bool,
35 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, }
46 }
47}
48
49impl PipelineConfig {
50 pub fn healthcare() -> Self {
53 PipelineConfig {
54 depth_limit: 16,
55 exhaustive_evaluation: true,
56 confidence_threshold: 204, }
58 }
59
60 pub fn embedded() -> Self {
63 PipelineConfig {
64 depth_limit: 8,
65 exhaustive_evaluation: false,
66 confidence_threshold: 51, }
68 }
69}
70
71pub 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 #[cfg(feature = "alloc")]
99 pub fn evaluate_str(&self, expression: &str, ctx: &EvalContext<'_>) -> Verdict {
100 let mut trace = LogicTrace::new();
101
102 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 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 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 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 pub fn evaluate_ast(
164 &self,
165 ast: &AstNode,
166 active_paradigms: ParadigmSet,
167 ctx: &EvalContext<'_>,
168 mut trace: LogicTrace,
169 ) -> Verdict {
170 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 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 let threshold_met = confidence.0 >= self.config.confidence_threshold;
215
216 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 let verdict = pipeline.evaluate_str("must true", &ctx);
325 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}