# `StatifierUI.TruthTable`
[🔗](https://github.com/riddler/statifier-ui/blob/v0.11.0/lib/statifier_ui/truth_table.ex#L1)

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.

# `cell`

```elixir
@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`

```elixir
@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`

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

# `t`

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

# `verdict`

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

# `build`

```elixir
@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`

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

# `cells`

```elixir
@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`

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

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

# `label`

```elixir
@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`

```elixir
@spec row(t(), StatifierUI.Fixtures.expression_name()) :: [cell()]
```

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
