StatifierUI.TruthTable (StatifierUI v0.9.1)

Copy Markdown View Source

Evaluates a fixture bundle's expressions across its datasets and returns the result matrix ADR-0006 named: "one expression evaluated across all datasets, rendered as a result matrix".

This module is the matrix; StatifierUI.TruthTable.Markdown renders it. Nothing here touches Kino, Phoenix, or a chart - a truth table is a fact about expressions and datasets, so it is buildable from a bundle alone.

Three values, never two

A cell's verdict is one of :satisfied, :unsatisfied, :undefined, :value, :error, or :missing_dataset. The first three are deliberately not true / false / :undefined: predicator's undefined sentinel is a third truth value, and an atom-vs-boolean encoding is what lets if cell.verdict do quietly sort :undefined onto one side of a two-valued fence. Sparse records make undefined the common case, so collapsing it is exactly the misreading this table exists to prevent, and the type is shaped so the collapse cannot happen by accident.

:value is a non-boolean result - predicator expressions are not required to be predicates, and "signup.started_at" is a perfectly good row. :error is an evaluation that returned {:error, e}; the error is carried as data (CLAUDE.md's errors-are-values rule) rather than raised or flattened into a false.

Relationship to StatifierUI.Fixtures.Expectations

Distinct questions over the same bundle. Expectations asks whether every stated expect value still holds, and is a test helper. This asks what every expression actually evaluates to under every dataset, whether or not an expectation was stated - the executable-documentation surface a reader scans. A bundle with no expect keys at all still has a full truth table.

Because of that, this module reads no expect value and takes no position on expect-related severity.

Building

{:ok, fixtures} = StatifierUI.Fixtures.from_source(MyApp.Fixtures)
table = StatifierUI.TruthTable.build(fixtures)

:expressions and :datasets narrow either axis to a named subset, in the order given, for a table too wide to read whole. A name on either list that the bundle does not carry is kept on the axis rather than dropped: a silently missing column is a worse read than a column that says so, and its cells carry :missing_dataset (or, for an unknown expression, an :unknown_expression error) instead.

Summary

Types

One cell of the matrix.

One row of the expression axis: its name and the source it evaluates.

t()

What one expression evaluated to under one dataset.

Functions

Builds the matrix for fixtures.

Fetches the cell at (expression_name, dataset_name).

Every cell, in expression-then-dataset axis order.

The cells of one dataset's column, in expression-axis order.

The spelled-out label for a verdict.

The cells of one expression's row, in dataset-axis order.

Types

cell()

@type cell() :: %{
  expression: StatifierUI.Fixtures.expression_name(),
  dataset: StatifierUI.Fixtures.dataset_name(),
  source: String.t() | nil,
  verdict: verdict(),
  value: term() | nil,
  label: String.t(),
  error: term() | nil
}

One cell of the matrix.

value is the term Predicator.evaluate/3 returned, nil when nothing was evaluated. label is the human-readable rendering of the verdict, always a spelled-out word rather than a symbol, so a renderer can add emphasis without the text ever depending on it.

expression()

@type expression() :: %{
  name: StatifierUI.Fixtures.expression_name(),
  source: String.t() | nil
}

One row of the expression axis: its name and the source it evaluates.

opt()

@type opt() ::
  {:expressions, [StatifierUI.Fixtures.expression_name()]}
  | {:datasets, [StatifierUI.Fixtures.dataset_name()]}
  | {:functions, map()}
  | {:providers, keyword()}

t()

@type t() :: %StatifierUI.TruthTable{
  cells: %{
    optional(
      {StatifierUI.Fixtures.expression_name(),
       StatifierUI.Fixtures.dataset_name()}
    ) => cell()
  },
  datasets: [StatifierUI.Fixtures.dataset_name()],
  expressions: [expression()]
}

verdict()

@type verdict() ::
  :satisfied | :unsatisfied | :undefined | :value | :error | :missing_dataset

What one expression evaluated to under one dataset.

See the moduledoc for why the boolean verdicts are atoms rather than true / false.

Functions

build(fixtures, opts \\ [])

@spec build(StatifierUI.Fixtures.t(), [opt()]) :: t()

Builds the matrix for fixtures.

Options:

  • :expressions - the expression axis, in order. Defaults to every expression the bundle carries, sorted.
  • :datasets - the dataset axis, in order. Defaults to every dataset the bundle carries, sorted.
  • :functions, :providers - forwarded verbatim to Predicator.evaluate/3; nothing else is a truth-table concern.

Never raises: an evaluation failure is an :error cell.

cell(truth_table, expression_name, dataset_name)

Fetches the cell at (expression_name, dataset_name).

Returns :error when either name is off the table's axes - which is not the same as a cell whose verdict is :missing_dataset, where the name is on the axis and the bundle has no such dataset.

cells(table)

@spec cells(t()) :: [cell()]

Every cell, in expression-then-dataset axis order.

Stable across runs, because both axes are ordered lists rather than map keys.

column(table, dataset_name)

@spec column(t(), StatifierUI.Fixtures.dataset_name()) :: [cell()]

The cells of one dataset's column, in expression-axis order.

label(atom)

@spec label(verdict()) :: String.t()

The spelled-out label for a verdict.

Public because a renderer other than the bundled Markdown one should reuse the same words rather than invent a second vocabulary for the same six states.

row(table, expression_name)

The cells of one expression's row, in dataset-axis order.