1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
//! aitia, a crate for gaining insights from causal graphs
//!
//! ("aitia" is the root of "etiology", the study of causes, and also an easy-to-type palindrome)
//!
//! In complex systems, when something is going wrong, it can be difficult to
//! narrow down the dep to some subsystem without doing lots of forensic
//! poking around and exploratory logging. `aitia` aims to help with this process
//! of narrowing down the possible scope of a problem.
//!
//! You define a collection of [`Fact`]s about your system, each of which specifies
//! one or more [`Dep`]s (dependencies). The causal relationships between Depsimply a graph,
//! with each Fact connected to others. When testing your system, you can check whether a
//! particular Fact is true or not. If it's not true, `aitia` recursively follows the causal
//! relationships specified, building up a graph of deps, each of which is checked for
//! truth. The traversal stops only when either:
//! 1. a true fact is encountered, or
//! 2. a fact without any deps of its own is encountered (an "axiom" so to speak)
//! 3. a loop of unresolved deps is discovered, in which case that entire branch is discarded.
//! Note that loops in causal relationships are allowed, as long as there is a Fact
//! along the loop which passes, causing the loop to be broken
//!
//! The result is a directed acyclic graph (DAG) of all facts that are not true, with the original fact as the root.
//! The edges of the graph represent [logical conditionals or implications](https://en.wikipedia.org/wiki/Material_conditional):
//! i.e. an edge A -> B in the graph means
//! "If A is true, then B must be true", and also "if B is not true, then A cannot be true".
//! This means that the leaves of the DAG represent the possible root deps of the problem, i.e. the
//! "most upstream" known facts which could dep the root fact to not be true, and so the leaves
//! would represent the places in the system to look for the reason why your original fact was not true.
//!
//! `aitia` is as useful as the Facts you write. You can write very broad, vague facts, which can
//! help you hone in on broader parts of the system for further manual investigation, or you can
//! write very specific facts which can tell you at a glance what may be the problem. It lends
//! itself well to writing facts iteratively, broadly at first, and then adding more specificity
//! as you do the work of diagnosing the problems that it helped you find.
//!
//! `aitia` is meant to be an embodiment of the process of deducing the dep of a problem.
//! By encoding your search for a problem into an `aitia::Dep`, ideally you will never have to
//! hunt for that particular problem again.
// #![warn(missing_docs)]
mod dep;
mod fact;
mod graph;
pub(crate) mod traversal;
#[macro_use]
#[cfg(feature = "tracing")]
pub mod logging;
use std::collections::HashSet;
pub use dep::{Dep, DepError, DepResult};
pub use fact::{Fact, FactTraits};
#[cfg(test)]
mod tests;
use traversal::{Traversal, TraversalError, TraversalResult};
#[macro_export]
macro_rules! assert_fact {
($ctx: expr, $fact: expr) => {{
use $crate::Fact;
let tr = $fact.clone().traverse($ctx);
if let Some(report) = $crate::simple_report(&tr) {
panic!("{report}");
}
}};
}
pub enum TraversalOutcome<'c, F: Fact> {
/// The fact was true and all dependencies were true
Success,
/// The fact was not true
DependencyNotMet,
/// The fact was true, but some dependencies were not, which indicates an incorrect model
IncorrectModel(&'c HashSet<Dep<F>>),
}
impl<'c, F: Fact> TraversalOutcome<'c, F> {
pub fn report(&self) -> Option<String> {
match self {
TraversalOutcome::Success => None,
TraversalOutcome::DependencyNotMet => Some("aitia dependency not met given the context".to_string()) ,
TraversalOutcome::IncorrectModel(deps) => Some(format!("Target fact was true, but some dependency checks failed. Your model may be incorrect. Failed checks: {deps:?}")) ,
}
}
pub fn from_traversal(tr: &'c Traversal<'c, F>) -> Self {
let Traversal {
root_check_passed,
terminals,
..
} = tr;
if *root_check_passed {
if terminals.is_empty() {
// All is well
TraversalOutcome::Success
} else {
TraversalOutcome::IncorrectModel(terminals)
}
} else {
TraversalOutcome::DependencyNotMet
}
}
}
/// Helpful function for printing a report from a given Traversal
///
/// You're encouraged to write your own reports as best serve you, but this
/// is a good starting point.
#[must_use]
pub fn simple_report<T: Fact>(tr: &TraversalResult<T>) -> Option<String> {
use std::fmt::Write;
match tr {
Ok(tr) => {
let mut out = "".to_string();
let outcome = TraversalOutcome::from_traversal(tr);
if let Some(problem) = outcome.report() {
writeln!(&mut out, "The targed fact FAILED: {problem}").unwrap();
writeln!(&mut out, "{}", tr.graph.report().unwrap()).unwrap();
let terminals: Vec<_> = tr.terminals.iter().map(|p| p.explain(tr.ctx)).collect();
writeln!(&mut out, "Terminal nodes:").unwrap();
for term in terminals {
writeln!(&mut out, "{term}").unwrap();
}
Some(out)
} else {
None
}
}
Err(TraversalError { graph, inner }) => {
let mut out = "".to_string();
writeln!(&mut out, "{}", graph.report().unwrap()).unwrap();
writeln!(&mut out, "Traversal error: {inner:?}").unwrap();
Some(out)
}
}
}
pub fn print_simple_report<T: Fact>(tr: &TraversalResult<T>) {
if let Some(report) = simple_report(tr) {
println!("{report}");
} else {
println!("The target fact PASSED");
}
}