Asking a session why.
Truth maintenance already records which match inserted what, and 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 rather than 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 rather than 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 it the node id of the accumulate node, which why_not/2
reports, 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.
Reads truth maintenance, so it reports what is currently concluded rather than a
history. A rule that fired and was later retracted does not appear. Attach
Rete.Listener.Collect and read :activation_fired events for that.
Generated negation helpers are excluded unless 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.
The question behind "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 is "matches that got through", and the two mean different things per node
kind, so 0 in one column is not by itself a failure. Compare a node with the one
before it. See docs/design/observability.md §2.