Rete.Inspect (Rete v0.5.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 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.

Fire before you inspect. why_not/2 and collection/3 read what propagation built, and Rete.Session.insert/2 only queues propagation. On a session that never fired they would report zero of everything. That reads as "nothing matched", but the truth is "nothing has been matched yet". Both raise instead.

explain/2 and fired/2 read memories that insert/2 and retract/2 update at once, so both answer at any point. They answer about the session as it stands, though, not as it will stand. A queued retract takes the fact out of working memory and leaves the conclusions that rest on it, so explain/2 shows a support that already left, reported as origin: :unknown. That is true of the session now, and it stops being true on the next fire. Fire first for a settled provenance graph.

Rete.Session.settled?/1 reports whether a session needs a fire.

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.

Which index a query would use for a set of filters, or :scan.

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

query_plan(session, ref, filters \\ [])

@spec query_plan(
  Rete.Session.t(),
  {module(), atom()},
  keyword() | %{required(atom()) => term()}
) ::
  {:index, [atom()]} | :scan

Which index a query would use for a set of filters, or :scan.

Rete.Ruleset.index/2 changes speed and nothing else, so an index that no call matches is invisible: the query answers correctly and stays as slow as it was. This is how to check that a declared index is the one a call reaches for.

A filter naming a superset of an index still uses it, and then narrows the bucket. A filter naming less than any declared index scans.

Rete.Inspect.query_plan(session, {MyApp.Orders, :flagged_for}, cid: 1)
#=> {:index, [:cid]}

Rete.Inspect.query_plan(session, {MyApp.Orders, :flagged_for}, amt: 250)
#=> :scan

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.