Rete.Inspect (Rete v0.9.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 two functions walk it.

Rete.Inspect.explain(session)
Rete.Inspect.explain(session, {MyRuleset, :large_order})
Rete.Inspect.why_not(session, {MyRuleset, :some_rule})

explain/1,2 reports what happened: every match a rule fired on, the facts behind that match, and the facts it concluded. why_not/1,2 reports where a rule stopped, condition by condition. Give either one a {module, name} pair for one rule. Give it no pair for every rule and query in the session.

Both work with no listener and no setup, because they read 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. Both functions 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. 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

One match a rule fired on.

How far one rule got, node by node.

What one rule or query did.

One fact behind a match, and where it came from.

A rule or query, named the way Rete.Session.query/3 names one.

Functions

What every rule and query in the session did.

What one rule or query did, match by match.

How far every rule and query got.

How far a rule got, condition by condition.

Types

activation()

@type activation() :: %{
  bindings: %{required(atom()) => term()},
  matches: [match()],
  inserted: [term()]
}

One match a rule fired on.

:inserted is what the rule concluded from this match. It is [] on a query, because a query concludes nothing.

chain()

@type chain() :: %{rule: atom(), module: module(), chain: [map()]}

How far one rule got, node by node.

explanation()

@type explanation() :: %{
  rule: atom(),
  module: module(),
  type: :rule | :query,
  activations: [activation()]
}

What one rule or query did.

match()

@type match() :: %{
  fact: term(),
  origin: :asserted | :derived | :gathered,
  from: [ref()],
  members: [match()] | nil
}

One fact behind a match, and where it came from.

:origin is :asserted for a fact you inserted, :derived for one a rule concluded, and :gathered for a collection. :from names the rules that concluded the fact. It is a list, because a fact can rest on more than one independent support, and it is [] unless :origin is :derived.

:members is nil, except on a collection. There :fact is the gathered list, which is what the rule received, and :members describes each fact in it.

ref()

@type ref() :: {module(), atom()}

A rule or query, named the way Rete.Session.query/3 names one.

Functions

explain(session)

@spec explain(Rete.Session.t()) :: [explanation()]

What every rule and query in the session did.

One entry per rule and per query, sorted by module and then by name. Generated negation helpers are left out. Name one with explain/2 to see it anyway.

iex> alias Rete.{Inspect, Session}
iex> Session.new([Rete.Doc.Orders])
...> |> Session.insert([{:customer, 1}, {:order, 1, 250}])
...> |> Session.fire_rules()
...> |> Inspect.explain()
...> |> Enum.map(&{&1.type, &1.rule, length(&1.activations)})
[{:query, :flagged_for, 1}, {:rule, :large_order, 1}]

explain(session, ref)

@spec explain(Rete.Session.t(), ref()) :: explanation()

What one rule or query did, match by match.

Each activation is one match the rule fired on. :bindings is what that match bound, :matches is the facts behind it, and :inserted is what the rule concluded from it.

activations: [] means the rule concluded nothing. That is usually a rule that never matched, and why_not/2 then says where it stopped. A rule whose body returns nil or [] reports the same, because nothing in memory records a match no conclusion rests on. Rete.Listener sees that firing where this cannot.

Each entry of :matches says where its fact came from. :from names the rules that concluded it, so you read a chain by following that pair to its own entry in the same result. A collection reports the gathered list as its fact, and describes each member under :members.

Activations are sorted by their bindings, and not by the order they fired in. Two that bind the same values keep an order that carries no meaning. One session gives one answer, so a test may rely on the list. Attach Rete.Listener.Collect for the order they fired in.

iex> alias Rete.{Inspect, Session}
iex> session =
...>   Session.new([Rete.Doc.Orders])
...>   |> Session.insert([{:customer, 1}, {:order, 1, 250}])
...>   |> Session.fire_rules()
iex> %{activations: [activation]} =
...>   Inspect.explain(session, {Rete.Doc.Orders, :large_order})
iex> activation.inserted
[{:flagged, 1, 250}]
iex> Enum.map(activation.matches, &{&1.fact, &1.origin})
[{{:customer, 1}, :asserted}, {{:order, 1, 250}, :asserted}]

The query over what that rule concluded names the rule as the source of its own match:

iex> alias Rete.{Inspect, Session}
iex> session =
...>   Session.new([Rete.Doc.Orders])
...>   |> Session.insert([{:customer, 1}, {:order, 1, 250}])
...>   |> Session.fire_rules()
iex> %{type: :query, activations: [%{matches: [match]}]} =
...>   Inspect.explain(session, {Rete.Doc.Orders, :flagged_for})
iex> {match.fact, match.origin, match.from}
{{:flagged, 1, 250}, :derived, [{Rete.Doc.Orders, :large_order}]}

why_not(session)

@spec why_not(Rete.Session.t()) :: [chain()]

How far every rule and query got.

One entry per rule and per query, sorted by module and then by name. Generated negation helpers are left out. Name one with why_not/2 to see it anyway.

why_not(session, ref)

@spec why_not(Rete.Session.t(), ref()) :: chain()

How far a rule got, condition by condition.

This answers "why did this not fire?". Each entry of :chain reports what one node on the chain of the rule holds. :elements are the facts that matched this condition alone. :tokens are partial matches from the left. :activations, on terminals only, is the number of matches that it concluded from.

%{rule: :large_order, module: MyRuleset,
  chain: [
    %{node: 1, kind: "root_join", type: :customer, elements: 3, tokens: 0},
    %{node: 2, kind: "hash_join", type: :order,    elements: 0, tokens: 3}
  ]}

Read the chain 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.