Rete.Inspect (Rete v0.1.0)

Copy Markdown View Source

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

explanation()

@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

collection(session, node_id, join_key)

@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.

explain(session, fact)

@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}]}

fired(session, opts \\ [])

@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}]}]

why_not(session, ref)

@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.