Asking a session why.
Truth maintenance already records which match inserted what. Read backwards, that is a provenance graph. These functions walk it.
Rete.Inspect.explain(session, {:escalated, 1})
Rete.Inspect.fired(session)
Rete.Inspect.why_not(session, {MyRuleset, :some_rule})Everything here works on any session, with no listener and no setup, because it
reads working memory instead of a history. A listener adds what memory cannot know:
what happened, in what order, including activations that fired and were later
retracted. See Rete.Listener.
A rule is named by {module, name}, the identity Rete.Session.query/3 also uses.
Marker facts and the empty root token are engine machinery. An explanation describes
them rather than presenting them as matched facts. See
docs/design/observability.md §2.
Summary
Types
Why one fact exists.
Functions
The facts a collection gathered behind a token that came from it.
Why a fact exists, recursively down to the facts you asserted.
Every rule that has concluded something, with the match and what it inserted.
How far a rule got, condition by condition.
Types
@type explanation() :: %{ fact: term(), origin: :asserted | :derived | :unknown, rule: atom() | nil, module: module() | nil, bindings: map() | nil, supports: [explanation()] }
Why one fact exists.
:origin is :asserted, :derived, or :unknown — when the session does not hold
the fact. :rule, :module, and :bindings are nil when asserted. :supports
holds one nested explanation per fact the match rested on. :module is reported
alongside :rule, instead of folded into it, so a caller matching on a bare name still
works.
Functions
@spec collection(Rete.Session.t(), term(), map()) :: [term()]
The facts a collection gathered behind a token that came from it.
A collection propagates only its result, so the members are otherwise invisible, once
a token has moved on. Give this the node id of the accumulate node — why_not/2
reports it — and the join key from the token.
@spec explain(Rete.Session.t(), term()) :: [explanation()]
Why a fact exists, recursively down to the facts you asserted.
Returns a list, one entry per independent support. A fact can have more than one,
and removing one does not remove the fact. An asserted fact gives one entry with no
supports. A fact the session does not hold gives one with origin: :unknown.
iex> alias Rete.{Inspect, Session}
iex> session =
...> Session.new([Rete.Doc.Orders])
...> |> Session.insert([{:customer, 1}, {:order, 1, 250}])
...> |> Session.fire_rules()
iex> [%{origin: origin, rule: rule, supports: supports}] =
...> Inspect.explain(session, {:flagged, 1, 250})
iex> {origin, rule, supports |> Enum.map(& &1.fact) |> Enum.sort()}
{:derived, :large_order, [{:customer, 1}, {:order, 1, 250}]}
@spec fired( Rete.Session.t(), keyword() ) :: [%{rule: atom(), module: module(), bindings: map(), inserted: [term()]}]
Every rule that has concluded something, with the match and what it inserted.
This reads truth maintenance, so it reports what is currently concluded, instead of
a history. A rule that fired and was later retracted does not appear. Attach
Rete.Listener.Collect, and read :activation_fired events for that instead.
Generated negation helpers are excluded, unless you pass generated: true.
iex> alias Rete.{Inspect, Session}
iex> Session.new([Rete.Doc.Orders])
...> |> Session.insert([{:customer, 1}, {:order, 1, 250}])
...> |> Session.fire_rules()
...> |> Inspect.fired()
[%{rule: :large_order, module: Rete.Doc.Orders,
bindings: %{amt: 250, cid: 1}, inserted: [{:flagged, 1, 250}]}]
@spec why_not( Rete.Session.t(), {module(), atom()} ) :: [map()]
How far a rule got, condition by condition.
This answers "why did this not fire?". Each entry reports what one node on the rule's
chain holds: :elements are facts that matched this condition alone, :tokens are
partial matches from the left, and :activations (terminals only) is how many matches
it concluded from.
[%{node: 1, kind: "root_join", type: :customer, elements: 3, tokens: 0},
%{node: 2, kind: "hash_join", type: :order, elements: 0, tokens: 3}]Read it in order, and find the first node where the two counts disagree. Above, three customers reached the order condition, and no order matched them.
Neither count means "matches that got through". The two mean different things per node
kind, so 0 in one column is not by itself a failure. Compare a node against the one
before it instead. See docs/design/observability.md §2.