Rete.Inspect (Rete v0.2.0)

Copy Markdown View Source

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

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, instead of 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 this the node id of the accumulate node — why_not/2 reports it — 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.

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

why_not(session, ref)

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