use urge_core::{
ast::AstNode,
decision::{EntryOutcome, LogicTrace, Stage, TraceEntry, Verdict},
engine::{EvalContext, Paradigm},
symbol::{ParadigmSet, UnicodeSemanticDictionary},
};
use crate::{
parser::Parser, router::EngineRouter, tokenizer::Tokenizer, validator::CrossValidator,
};
#[cfg(feature = "alloc")]
use alloc::string::String;
#[derive(Debug, Clone)]
pub struct PipelineConfig {
pub depth_limit: u8,
pub exhaustive_evaluation: bool,
pub confidence_threshold: u8,
}
impl Default for PipelineConfig {
fn default() -> Self {
PipelineConfig {
depth_limit: 32,
exhaustive_evaluation: false,
confidence_threshold: 128, }
}
}
impl PipelineConfig {
pub fn healthcare() -> Self {
PipelineConfig {
depth_limit: 16,
exhaustive_evaluation: true,
confidence_threshold: 204, }
}
pub fn embedded() -> Self {
PipelineConfig {
depth_limit: 8,
exhaustive_evaluation: false,
confidence_threshold: 51, }
}
}
pub struct GovernancePipeline {
pub config: PipelineConfig,
tokenizer: Tokenizer,
}
impl GovernancePipeline {
pub fn new(config: PipelineConfig) -> Self {
GovernancePipeline {
config,
tokenizer: Tokenizer::new(),
}
}
pub fn default_healthcare() -> Self {
Self::new(PipelineConfig::healthcare())
}
pub fn default_embedded() -> Self {
Self::new(PipelineConfig::embedded())
}
#[cfg(feature = "alloc")]
pub fn evaluate_str(&self, expression: &str, ctx: &EvalContext<'_>) -> Verdict {
let mut trace = LogicTrace::new();
trace.push(TraceEntry {
stage: Stage::Tokenization,
paradigm: None,
description: "tokenizing input",
outcome: EntryOutcome::Evaluated,
});
let tokens = self.tokenizer.tokenize(expression);
if tokens.is_empty() {
return Verdict::deny_immediate("empty or unrecognized expression");
}
trace.push(TraceEntry {
stage: Stage::ParadigmDetection,
paradigm: None,
description: "detecting active paradigms",
outcome: EntryOutcome::Evaluated,
});
let classes: alloc::vec::Vec<_> = tokens.iter().map(|t| t.class).collect();
let active_paradigms = if self.config.exhaustive_evaluation {
let mut all = ParadigmSet::empty();
for &p in Paradigm::ALL {
all.insert(p);
}
all
} else {
UnicodeSemanticDictionary::detect_paradigms(&classes)
};
for p in active_paradigms.iter() {
trace.push(TraceEntry {
stage: Stage::ParadigmDetection,
paradigm: Some(p),
description: p.name(),
outcome: EntryOutcome::Evaluated,
});
}
trace.push(TraceEntry {
stage: Stage::AstConstruction,
paradigm: None,
description: "building AST",
outcome: EntryOutcome::Evaluated,
});
let mut parser = Parser::new(tokens);
let ast = match parser.parse() {
Some(a) => a,
None => return Verdict::deny_immediate("failed to parse expression"),
};
self.evaluate_ast(&ast, active_paradigms, ctx, trace)
}
pub fn evaluate_ast(
&self,
ast: &AstNode,
active_paradigms: ParadigmSet,
ctx: &EvalContext<'_>,
mut trace: LogicTrace,
) -> Verdict {
trace.push(TraceEntry {
stage: Stage::EngineRouting,
paradigm: None,
description: "routing to engines",
outcome: EntryOutcome::Evaluated,
});
#[cfg(feature = "alloc")]
let verdicts = EngineRouter::route(ast, active_paradigms, ctx, &mut trace);
#[cfg(not(feature = "alloc"))]
let single_verdict = EngineRouter::route_single(ast, active_paradigms, ctx, &mut trace);
trace.push(TraceEntry {
stage: Stage::CrossValidation,
paradigm: None,
description: "cross-system validation",
outcome: EntryOutcome::Evaluated,
});
#[cfg(feature = "alloc")]
let (final_valid, confidence, cross_validation) =
CrossValidator::validate(&verdicts, &mut trace);
#[cfg(not(feature = "alloc"))]
let (final_valid, confidence, cross_validation) = match &single_verdict {
Ok(v) => {
let cv = CrossValidator::validate_single(v, &mut trace);
(v.valid, v.confidence, cv)
}
Err(_) => (
false,
Confidence::NONE,
CrossValidation {
consistent: false,
conflicts_detected: 1,
conflict_detail: Some("engine error"),
},
),
};
let threshold_met = confidence.0 >= self.config.confidence_threshold;
trace.push(TraceEntry {
stage: Stage::VerdictSynthesis,
paradigm: None,
description: if threshold_met {
"confidence threshold met"
} else {
"confidence threshold not met — deny"
},
outcome: if final_valid && threshold_met {
EntryOutcome::Permitted
} else {
EntryOutcome::Denied
},
});
Verdict {
valid: final_valid && threshold_met,
confidence,
paradigms_evaluated: active_paradigms,
trace,
cross_validation,
#[cfg(feature = "alloc")]
formal_notation: synthesize_notation(&verdicts),
#[cfg(feature = "alloc")]
citations: collect_citations(&verdicts),
}
}
}
#[cfg(feature = "alloc")]
fn synthesize_notation(verdicts: &[Result<Verdict, urge_core::engine::EngineError>]) -> String {
let notations: alloc::vec::Vec<String> = verdicts
.iter()
.filter_map(|v| v.as_ref().ok())
.map(|v| v.formal_notation.clone())
.filter(|s| !s.is_empty())
.collect();
if notations.is_empty() {
String::from("(no notation)")
} else {
notations.join(" ∧ ")
}
}
#[cfg(feature = "alloc")]
fn collect_citations(
verdicts: &[Result<Verdict, urge_core::engine::EngineError>],
) -> alloc::vec::Vec<urge_core::decision::Citation> {
verdicts
.iter()
.filter_map(|v| v.as_ref().ok())
.flat_map(|v| v.citations.iter().cloned())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use urge_core::engine::{ContextValue, EvalContext};
fn empty_ctx() -> EvalContext<'static> {
EvalContext {
slots: &[],
logical_time: 0,
depth_limit: 16,
}
}
#[test]
#[cfg(feature = "alloc")]
fn evaluate_simple_true() {
let pipeline = GovernancePipeline::new(PipelineConfig::default());
let ctx = empty_ctx();
let verdict = pipeline.evaluate_str("true", &ctx);
assert!(verdict.valid);
assert!(verdict.confidence.0 > 0);
}
#[test]
#[cfg(feature = "alloc")]
fn evaluate_simple_false() {
let pipeline = GovernancePipeline::new(PipelineConfig::default());
let ctx = empty_ctx();
let verdict = pipeline.evaluate_str("false", &ctx);
assert!(!verdict.valid);
}
#[test]
#[cfg(feature = "alloc")]
fn evaluate_negation() {
let pipeline = GovernancePipeline::new(PipelineConfig::default());
let ctx = empty_ctx();
let verdict = pipeline.evaluate_str("not false", &ctx);
assert!(verdict.valid);
}
#[test]
#[cfg(feature = "alloc")]
fn evaluate_deontic_obligation() {
let pipeline = GovernancePipeline::new(PipelineConfig::default());
let slots = &[("consent:done", ContextValue::Bool(true))];
let ctx = EvalContext {
slots,
logical_time: 0,
depth_limit: 16,
};
let verdict = pipeline.evaluate_str("must true", &ctx);
assert!(verdict.paradigms_evaluated.contains(Paradigm::Deontic));
}
#[test]
#[cfg(feature = "alloc")]
fn healthcare_config_exhaustive() {
let pipeline = GovernancePipeline::default_healthcare();
assert!(pipeline.config.exhaustive_evaluation);
assert!(pipeline.config.confidence_threshold > 128);
}
#[test]
#[cfg(feature = "alloc")]
fn trace_is_non_empty() {
let pipeline = GovernancePipeline::new(PipelineConfig::default());
let ctx = empty_ctx();
let verdict = pipeline.evaluate_str("always true", &ctx);
assert!(!verdict.trace.is_empty());
}
}