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.
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
@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.
@type expression() :: %{ name: StatifierUI.Fixtures.expression_name(), source: String.t() | nil }
One row of the expression axis: its name and the source it evaluates.
@type opt() :: {:expressions, [StatifierUI.Fixtures.expression_name()]} | {:datasets, [StatifierUI.Fixtures.dataset_name()]} | {:functions, map()} | {:providers, keyword()}
@type t() :: %StatifierUI.TruthTable{ cells: %{ optional( {StatifierUI.Fixtures.expression_name(), StatifierUI.Fixtures.dataset_name()} ) => cell() }, datasets: [StatifierUI.Fixtures.dataset_name()], expressions: [expression()] }
@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
@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 toPredicator.evaluate/3; nothing else is a truth-table concern.
Never raises: an evaluation failure is an :error cell.
@spec cell( t(), StatifierUI.Fixtures.expression_name(), StatifierUI.Fixtures.dataset_name() ) :: {:ok, cell()} | :error
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.
Every cell, in expression-then-dataset axis order.
Stable across runs, because both axes are ordered lists rather than map keys.
@spec column(t(), StatifierUI.Fixtures.dataset_name()) :: [cell()]
The cells of one dataset's column, in expression-axis order.
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.
@spec row(t(), StatifierUI.Fixtures.expression_name()) :: [cell()]
The cells of one expression's row, in dataset-axis order.