1use std::collections::{BTreeMap, BTreeSet};
14use std::sync::Arc;
15
16use axioval_ir::contract::{GateCondition, ParameterValue, RuleOutcomeKind, Selector, TableRow};
17use axioval_ir::{Evidence, NotEvaluatedReason, Object, ObjectId, RuleId, Scope, SourceId};
18
19use crate::{CapabilityEvaluation, CompiledRule, OutcomeRefiner, ResourceObjects, RuleContext};
20
21#[derive(Clone, Debug, PartialEq)]
24pub enum SelectorVerdict {
25 Match(Vec<Evidence>),
27 NoMatch(Vec<Evidence>),
29 Undecided(NotEvaluatedReason, String),
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub enum RuleVerdict {
36 Passed,
38 Failed,
40 Undecided,
42 Skipped,
44}
45
46#[derive(Clone, Debug, Eq, PartialEq)]
48pub enum ObjectVerdict {
49 Passed,
51 Failed,
53 NotSelected,
55 Undecided(String),
57}
58
59#[derive(Clone, Debug, Default)]
61pub struct RuleRecord {
62 skipped: bool,
63 found: bool,
64 open: bool,
65 failed: BTreeSet<ObjectId>,
67 undecided: BTreeMap<ObjectId, String>,
69 sources: BTreeMap<SourceId, String>,
71 project: Option<String>,
73 selection: Option<(BTreeSet<ObjectId>, BTreeMap<ObjectId, String>)>,
77 unread: BTreeSet<(SourceId, String)>,
80}
81
82impl RuleRecord {
83 pub(crate) fn skipped() -> Self {
85 Self {
86 skipped: true,
87 ..Self::default()
88 }
89 }
90
91 pub(crate) fn undecided(message: &str) -> Self {
93 Self {
94 open: true,
95 project: Some(message.to_owned()),
96 ..Self::default()
97 }
98 }
99
100 pub(crate) fn of(evaluation: &CapabilityEvaluation, selection: Option<Selection>) -> Self {
103 let mut record = Self {
104 found: !evaluation.findings().is_empty(),
105 open: !evaluation.not_evaluated_outcomes().is_empty(),
106 ..Self::default()
107 };
108 for finding in evaluation.findings() {
109 let message = format!("it reported `{}` about it", finding.message);
110 match &finding.scope {
111 Scope::Object(object) => {
112 record.failed.insert(object.clone());
113 }
114 scope => record.note_whole(scope, message),
115 }
116 }
117 for outcome in evaluation.not_evaluated_outcomes() {
118 match outcome.scope() {
119 Scope::Object(object) => {
120 record
121 .undecided
122 .entry(object.clone())
123 .or_insert_with(|| outcome.message().to_owned());
124 }
125 scope => record.note_whole(scope, outcome.message().to_owned()),
126 }
127 }
128 let selection = selection.map(|selection| {
129 record.unread = selection.unread.into_iter().collect();
130 selection.verdicts
131 });
132 record.selection = selection.map(|verdicts| {
133 let mut selected = BTreeSet::new();
134 let mut open = BTreeMap::new();
135 for (object, verdict) in verdicts {
136 match verdict {
137 SelectorVerdict::Match(_) => {
138 selected.insert(object);
139 }
140 SelectorVerdict::NoMatch(_) => {}
141 SelectorVerdict::Undecided(_, why) => {
142 open.insert(object, why);
143 }
144 }
145 }
146 (selected, open)
147 });
148 record
149 }
150
151 fn note_whole(&mut self, scope: &Scope, message: String) {
152 match scope {
153 Scope::Source(source) => {
154 self.sources.entry(source.clone()).or_insert(message);
155 }
156 _ => {
157 self.project.get_or_insert(message);
158 }
159 }
160 }
161
162 pub fn named(&self) -> impl Iterator<Item = &ObjectId> {
165 let (selected, open) = self
166 .selection
167 .as_ref()
168 .map(|(selected, open)| (Some(selected), Some(open)))
169 .unwrap_or_default();
170 self.failed
171 .iter()
172 .chain(self.undecided.keys())
173 .chain(selected.into_iter().flatten())
174 .chain(open.into_iter().flat_map(BTreeMap::keys))
175 }
176
177 pub fn unread_resources(&self) -> impl Iterator<Item = &(SourceId, String)> {
180 self.unread.iter()
181 }
182
183 #[must_use]
185 pub fn verdict(&self) -> RuleVerdict {
186 if self.skipped {
187 RuleVerdict::Skipped
188 } else if self.found {
189 RuleVerdict::Failed
190 } else if self.open {
191 RuleVerdict::Undecided
192 } else {
193 RuleVerdict::Passed
194 }
195 }
196
197 #[must_use]
206 pub fn object(&self, object: &Object) -> ObjectVerdict {
207 if self.failed.contains(&object.id) {
208 return ObjectVerdict::Failed;
209 }
210 if self.skipped {
211 return ObjectVerdict::NotSelected;
212 }
213 if let Some(why) = self.undecided.get(&object.id) {
214 return ObjectVerdict::Undecided(format!("it was not evaluated: {why}"));
215 }
216 let Some((selected, open)) = &self.selection else {
217 return ObjectVerdict::Undecided("its selection was not recorded".into());
218 };
219 let chosen = selected.contains(&object.id);
220 let doubt = open.get(&object.id);
221 if !chosen && doubt.is_none() {
222 return ObjectVerdict::NotSelected;
223 }
224 if let Some(why) = &self.project {
225 return ObjectVerdict::Undecided(format!("it reported about the whole model: {why}"));
226 }
227 if let Some(why) = self.sources.get(&object.id.source) {
228 return ObjectVerdict::Undecided(format!(
229 "it reported about source `{}` as a whole: {why}",
230 object.id.source
231 ));
232 }
233 match doubt {
234 Some(why) => ObjectVerdict::Undecided(format!("its selection is undecided: {why}")),
235 None => ObjectVerdict::Passed,
236 }
237 }
238}
239
240#[derive(Clone, Debug, Default)]
244pub struct RuleOutcomes(BTreeMap<RuleId, Arc<RuleRecord>>);
245
246impl RuleOutcomes {
247 #[must_use]
249 pub fn get(&self, rule: &str) -> Option<&RuleRecord> {
250 let id = RuleId::new(rule).ok()?;
251 self.0.get(&id).map(AsRef::as_ref)
252 }
253
254 pub(crate) fn insert(&mut self, rule: RuleId, record: RuleRecord) {
255 self.0.insert(rule, Arc::new(record));
256 }
257
258 pub(crate) fn gate(&self, gates: &[(RuleId, GateCondition)]) -> Gate {
261 let mut state = Gate::Open;
262 for (parent, condition) in gates {
263 let verdict = self
264 .0
265 .get(parent)
266 .map_or(RuleVerdict::Undecided, |record| record.verdict());
267 match gate(parent, *condition, verdict) {
268 Gate::Closed => return Gate::Closed,
269 undecided @ Gate::Undecided(_) if state == Gate::Open => state = undecided,
270 _ => {}
271 }
272 }
273 state
274 }
275
276 pub fn selects(
280 &self,
281 rule: &str,
282 outcome: RuleOutcomeKind,
283 object: &Object,
284 ) -> Result<bool, (NotEvaluatedReason, String)> {
285 let Some(record) = self.get(rule) else {
286 return Err((
287 NotEvaluatedReason::InvalidDeclaration,
288 format!("the outcomes of rule `{rule}` are not available: it has not run before"),
289 ));
290 };
291 match record.object(object) {
292 ObjectVerdict::Passed => Ok(outcome == RuleOutcomeKind::Passed),
293 ObjectVerdict::Failed => Ok(outcome == RuleOutcomeKind::Failed),
294 ObjectVerdict::NotSelected => Ok(false),
295 ObjectVerdict::Undecided(why) => Err((
296 NotEvaluatedReason::IncompleteEvidence,
297 format!("rule `{rule}` left {} undecided: {why}", object.id),
298 )),
299 }
300 }
301}
302
303pub(crate) struct Selection {
305 verdicts: Vec<(ObjectId, SelectorVerdict)>,
306 unread: Vec<(SourceId, String)>,
307}
308
309impl From<Vec<(ObjectId, SelectorVerdict)>> for Selection {
310 fn from(verdicts: Vec<(ObjectId, SelectorVerdict)>) -> Self {
312 Self {
313 verdicts,
314 unread: Vec::new(),
315 }
316 }
317}
318
319pub(crate) fn selection(
323 refiner: &dyn OutcomeRefiner,
324 context: &RuleContext<'_>,
325 rule: &CompiledRule,
326) -> Selection {
327 let reached = context
328 .services
329 .get::<ResourceObjects>()
330 .map(|resources| resources.reached(&rule.selector, context.services.get::<RuleOutcomes>()))
331 .unwrap_or_default();
332 let verdicts = context
333 .project
334 .objects()
335 .chain(reached.objects)
336 .map(|object| {
337 let verdict = refiner.evaluate_selector(context, &rule.selector, object);
338 (object.id.clone(), verdict)
339 })
340 .collect();
341 Selection {
342 verdicts,
343 unread: reached.unreadable,
344 }
345}
346
347#[derive(Clone, Debug, Eq, PartialEq)]
349pub(crate) enum Gate {
350 Open,
351 Closed,
352 Undecided(String),
353}
354
355pub(crate) fn gate(parent: &RuleId, condition: GateCondition, verdict: RuleVerdict) -> Gate {
358 let wants = match condition {
359 GateCondition::AllIfPassed => RuleVerdict::Passed,
360 GateCondition::AllIfFailed => RuleVerdict::Failed,
361 GateCondition::PassedObjects | GateCondition::FailedObjects => return Gate::Open,
363 };
364 match verdict {
365 RuleVerdict::Undecided => Gate::Undecided(format!(
366 "the rule runs only if rule `{parent}` {}, and `{parent}` left outcomes not \
367 evaluated without a finding, so whether it passed is undecided",
368 if wants == RuleVerdict::Passed {
369 "passed"
370 } else {
371 "failed"
372 }
373 )),
374 verdict if verdict == wants => Gate::Open,
375 _ => Gate::Closed,
376 }
377}
378
379pub(crate) fn gate_selector(rule: &str, condition: GateCondition) -> Option<Selector> {
381 let outcome = match condition {
382 GateCondition::PassedObjects => RuleOutcomeKind::Passed,
383 GateCondition::FailedObjects => RuleOutcomeKind::Failed,
384 GateCondition::AllIfPassed | GateCondition::AllIfFailed => return None,
385 };
386 Some(Selector::RuleOutcome {
387 rule: rule.to_owned(),
388 outcome,
389 })
390}
391
392pub(crate) fn selector_references<'a>(selector: &'a Selector, out: &mut BTreeSet<&'a str>) {
394 match selector {
395 Selector::RuleOutcome { rule, .. } => {
396 out.insert(rule);
397 }
398 Selector::AllOf { operands } | Selector::AnyOf { operands } => {
399 for operand in operands {
400 selector_references(operand, out);
401 }
402 }
403 Selector::Not { operand } => selector_references(operand, out),
404 Selector::Related { selector, .. } => selector_references(selector, out),
405 Selector::All
406 | Selector::EntityType { .. }
407 | Selector::Property { .. }
408 | Selector::PropertyPattern { .. }
409 | Selector::Classification { .. }
410 | Selector::Discipline { .. }
411 | Selector::Source { .. } => {}
412 }
413}
414
415pub(crate) fn value_references<'a>(value: &'a ParameterValue, out: &mut BTreeSet<&'a str>) {
417 match value {
418 ParameterValue::Selector { value } => selector_references(value, out),
419 ParameterValue::Table { value: rows } => {
420 for cell in rows.iter().flat_map(TableRow::values) {
421 value_references(cell, out);
422 }
423 }
424 _ => {}
425 }
426}
427
428pub(crate) fn rename_selector(selector: &mut Selector, rename: &dyn Fn(&str) -> String) {
430 match selector {
431 Selector::RuleOutcome { rule, .. } => *rule = rename(rule),
432 Selector::AllOf { operands } | Selector::AnyOf { operands } => {
433 for operand in operands {
434 rename_selector(operand, rename);
435 }
436 }
437 Selector::Not { operand } => rename_selector(operand, rename),
438 Selector::Related { selector, .. } => rename_selector(selector, rename),
439 Selector::All
440 | Selector::EntityType { .. }
441 | Selector::Property { .. }
442 | Selector::PropertyPattern { .. }
443 | Selector::Classification { .. }
444 | Selector::Discipline { .. }
445 | Selector::Source { .. } => {}
446 }
447}
448
449pub(crate) fn rename_value(value: &mut ParameterValue, rename: &dyn Fn(&str) -> String) {
451 match value {
452 ParameterValue::Selector { value } => rename_selector(value, rename),
453 ParameterValue::Table { value: rows } => {
454 for cell in rows.iter_mut().flat_map(|row| row.values_mut()) {
455 rename_value(cell, rename);
456 }
457 }
458 _ => {}
459 }
460}
461
462pub(crate) fn dependency_order(
465 rules: Vec<CompiledRule>,
466 dependencies: &BTreeMap<RuleId, BTreeSet<RuleId>>,
467) -> Result<Vec<CompiledRule>, Vec<RuleId>> {
468 let mut pending: BTreeMap<RuleId, CompiledRule> = rules
469 .into_iter()
470 .map(|rule| (rule.id.clone(), rule))
471 .collect();
472 let mut ordered = Vec::with_capacity(pending.len());
473 loop {
474 let ready: Option<RuleId> = pending
475 .keys()
476 .find(|id| {
477 dependencies
478 .get(*id)
479 .is_none_or(|needs| needs.iter().all(|need| !pending.contains_key(need)))
480 })
481 .cloned();
482 match ready {
483 Some(id) => ordered.push(pending.remove(&id).expect("a ready rule is pending")),
484 None if pending.is_empty() => return Ok(ordered),
485 None => return Err(pending.into_keys().collect()),
486 }
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493 use axioval_ir::{Finding, Severity};
494
495 fn id(local: &str) -> ObjectId {
496 ObjectId::new(SourceId::new("test", "model").unwrap(), local).unwrap()
497 }
498
499 fn object(local: &str) -> Object {
500 Object::new(id(local), "door")
501 }
502
503 fn record() -> RuleRecord {
506 let mut evaluation = CapabilityEvaluation::default();
507 evaluation.push_finding(Finding::new(
508 RuleId::new("type").unwrap(),
509 id("d2"),
510 Severity::Error,
511 "no rating",
512 ));
513 evaluation.push_object_not_evaluated(
514 id("d3"),
515 NotEvaluatedReason::BackendUnavailable,
516 "unreadable",
517 );
518 let selection = vec![
519 (id("d1"), SelectorVerdict::Match(Vec::new())),
520 (id("d2"), SelectorVerdict::Match(Vec::new())),
521 (id("d3"), SelectorVerdict::Match(Vec::new())),
522 (id("d4"), SelectorVerdict::NoMatch(Vec::new())),
523 (
524 id("d5"),
525 SelectorVerdict::Undecided(NotEvaluatedReason::InvalidEvidence, "list".into()),
526 ),
527 ];
528 RuleRecord::of(&evaluation, Some(selection.into()))
529 }
530
531 #[test]
532 fn an_object_is_judged_by_its_finding_its_outcome_and_its_selection() {
533 let record = record();
534 assert_eq!(record.verdict(), RuleVerdict::Failed);
535 assert_eq!(record.object(&object("d1")), ObjectVerdict::Passed);
536 assert_eq!(record.object(&object("d2")), ObjectVerdict::Failed);
537 assert!(matches!(
538 record.object(&object("d3")),
539 ObjectVerdict::Undecided(_)
540 ));
541 assert_eq!(record.object(&object("d4")), ObjectVerdict::NotSelected);
542 assert!(matches!(
543 record.object(&object("d5")),
544 ObjectVerdict::Undecided(_)
545 ));
546 }
547
548 #[test]
549 fn an_outcome_about_the_source_leaves_its_selected_objects_undecided() {
550 let mut evaluation = CapabilityEvaluation::default();
551 evaluation.push_source_not_evaluated(
552 SourceId::new("test", "model").unwrap(),
553 NotEvaluatedReason::NotRecorded,
554 "no layers",
555 );
556 let record = RuleRecord::of(
557 &evaluation,
558 Some(
559 vec![
560 (id("d1"), SelectorVerdict::Match(Vec::new())),
561 (id("d4"), SelectorVerdict::NoMatch(Vec::new())),
562 ]
563 .into(),
564 ),
565 );
566 assert_eq!(record.verdict(), RuleVerdict::Undecided);
567 assert!(matches!(
568 record.object(&object("d1")),
569 ObjectVerdict::Undecided(_)
570 ));
571 assert_eq!(record.object(&object("d4")), ObjectVerdict::NotSelected);
572 let unrecorded = RuleRecord::of(&CapabilityEvaluation::default(), None);
574 assert!(matches!(
575 unrecorded.object(&object("d1")),
576 ObjectVerdict::Undecided(_)
577 ));
578 }
579
580 #[test]
581 fn whole_gates_open_close_or_stay_undecided_with_the_parent() {
582 let parent = RuleId::new("type").unwrap();
583 let passed = GateCondition::AllIfPassed;
584 assert_eq!(gate(&parent, passed, RuleVerdict::Passed), Gate::Open);
585 assert_eq!(gate(&parent, passed, RuleVerdict::Failed), Gate::Closed);
586 assert_eq!(gate(&parent, passed, RuleVerdict::Skipped), Gate::Closed);
587 assert!(matches!(
588 gate(&parent, GateCondition::AllIfFailed, RuleVerdict::Undecided),
589 Gate::Undecided(_)
590 ));
591 assert_eq!(
592 gate(
593 &parent,
594 GateCondition::FailedObjects,
595 RuleVerdict::Undecided
596 ),
597 Gate::Open
598 );
599 }
600}