termivar_scanner/rules/
evaluation.rs1use serde::Serialize;
2use std::collections::BTreeMap;
3use termivar_core::{BayesianEvidence, EvidenceId, Hypothesis};
4
5use crate::knowledge::{KnowledgeSnapshot, KnowledgeWrite};
6
7use crate::rules::{
8 expression::ExpressionEvaluation, hypothesis_id_for_rule, registry::ReasoningRule,
9 RuleEngineError,
10};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
14pub struct RuleEvaluation {
15 rule_id: String,
16 condition: ExpressionEvaluation,
17 hypothesis: Option<Hypothesis>,
18}
19
20impl RuleEvaluation {
21 pub fn rule_id(&self) -> &str {
23 &self.rule_id
24 }
25
26 pub fn matched(&self) -> bool {
28 self.condition.matched()
29 }
30
31 pub fn condition(&self) -> &ExpressionEvaluation {
33 &self.condition
34 }
35
36 pub fn hypothesis(&self) -> Option<&Hypothesis> {
38 self.hypothesis.as_ref()
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
44pub struct RuleApplication {
45 pub(super) evaluation: RuleEvaluation,
46 pub(super) write: Option<KnowledgeWrite>,
47}
48
49impl RuleApplication {
50 pub fn evaluation(&self) -> &RuleEvaluation {
57 &self.evaluation
58 }
59
60 pub fn write(&self) -> Option<KnowledgeWrite> {
62 self.write
63 }
64}
65
66pub(super) fn evaluate_rule(
67 rule: &ReasoningRule,
68 snapshot: &KnowledgeSnapshot,
69) -> Result<RuleEvaluation, RuleEngineError> {
70 let condition = rule.condition.evaluate(snapshot)?;
71 let hypothesis = if condition.matched() {
72 Some(materialize_hypothesis(rule, snapshot, &condition)?)
73 } else {
74 None
75 };
76 Ok(RuleEvaluation {
77 rule_id: rule.id.clone(),
78 condition,
79 hypothesis,
80 })
81}
82
83fn materialize_hypothesis(
84 rule: &ReasoningRule,
85 snapshot: &KnowledgeSnapshot,
86 condition: &ExpressionEvaluation,
87) -> Result<Hypothesis, RuleEngineError> {
88 let mut observations = BTreeMap::<EvidenceId, BayesianEvidence>::new();
89 for calibration in &rule.conclusion.calibrations {
90 let mut matches = snapshot
91 .evidence()
92 .iter()
93 .filter(|evidence| {
94 condition.evidence_ids().contains(evidence.id())
95 && calibration.selector.matches(evidence)
96 })
97 .collect::<Vec<_>>();
98 if let Some(limit) = calibration.aggregation.limit() {
99 matches.sort_by(|left, right| {
100 right
101 .reliability()
102 .cmp(&left.reliability())
103 .then_with(|| right.observed_at_ms().cmp(&left.observed_at_ms()))
104 .then_with(|| left.id().cmp(right.id()))
105 });
106 matches.truncate(limit);
107 }
108 for evidence in matches {
109 let observation = BayesianEvidence::new(
110 evidence.id().clone(),
111 calibration.likelihood_if_true,
112 calibration.likelihood_if_false,
113 calibration.rationale.clone(),
114 )?;
115 if let Some(existing) = observations.get(evidence.id()) {
116 if existing != &observation {
117 return Err(RuleEngineError::AmbiguousEvidenceCalibration {
118 rule_id: rule.id.clone(),
119 evidence_id: evidence.id().clone(),
120 });
121 }
122 } else {
123 observations.insert(evidence.id().clone(), observation);
124 }
125 }
126 }
127
128 if observations.is_empty() {
129 return Err(RuleEngineError::MissingCalibratedEvidence {
130 rule_id: rule.id.clone(),
131 });
132 }
133
134 let stable_id = hypothesis_id_for_rule(&rule.id, snapshot.subject());
135 let mut hypothesis = Hypothesis::with_id(
136 stable_id,
137 snapshot.subject().clone(),
138 rule.conclusion.predicate.clone(),
139 rule.conclusion.value.clone(),
140 rule.conclusion.prior,
141 )?;
142 for observation in observations.into_values() {
143 hypothesis.observe(observation)?;
144 }
145 hypothesis.set_strength(rule.conclusion.strength);
146 hypothesis.set_state(rule.conclusion.state);
147 Ok(hypothesis)
148}