# `Mix.Statifier.RegressionRegistry`
[🔗](https://github.com/riddler/statifier-ex/blob/v2.6.0/lib/mix/statifier/regression_registry.ex#L1)

Reads and writes `test/passing_tests.json`, the regression ratchet registry.

The registry names the tests that must always pass. It has three lists, one
per suite, and entries may be literal paths or globs:

    {
      "description": "...",
      "internal_tests": ["test/statifier/**/*_test.exs"],
      "last_updated": "2026-08-02",
      "scion_tests": [],
      "w3c_tests": []
    }

`mix test.regression` runs exactly what this file expands to; `mix
test.baseline` is the only supported way to grow it. This module holds the
pure part of both tasks - load, expand, categorize, add, encode - so the
tasks themselves stay thin wrappers around `System.cmd/3`.

Encoding is deliberately hand-rolled rather than delegated to a JSON pretty
printer: keys are sorted and arrays are one entry per line, so ratcheting a
test in produces a one-line diff.

# `category`

```elixir
@type category() :: :internal | :scion | :w3c
```

# `stats`

```elixir
@type stats() :: %{
  passing: non_neg_integer(),
  total: non_neg_integer(),
  percent: float() | nil
}
```

# `t`

```elixir
@type t() :: %{required(String.t()) =&gt; term()}
```

# `add`

```elixir
@spec add(registry :: t(), paths :: [String.t()], today :: Date.t()) ::
  {t(), [String.t()], [String.t()]}
```

Adds `paths` to the registry, dated `today`.

Returns `{registry, added, skipped}`. Internal tests are skipped: they are
covered by globs already, so listing them individually would only add churn.
Entries are deduplicated and sorted, and the ratchet never removes anything.

# `categorize`

```elixir
@spec categorize(path :: String.t()) :: category()
```

The suite a test file belongs to, decided by its path.

## Examples

    iex> Mix.Statifier.RegressionRegistry.categorize("test/scion_tests/basic/basic0_test.exs")
    :scion

    iex> Mix.Statifier.RegressionRegistry.categorize("test/statifier/document_test.exs")
    :internal

# `conformance_categories`

```elixir
@spec conformance_categories() :: [category()]
```

The conformance suites, the only categories the ratchet grows by hand.

# `corpus_files`

```elixir
@spec corpus_files(category :: category(), root :: String.t()) :: [String.t()]
```

Every test file on disk for a conformance `category`, sorted.

Used by `mix test.baseline` to find candidates the registry does not know
about yet. `root` prefixes the suite directory; it defaults to the project
root and exists so the tasks can be pointed at a fixture tree under test.

# `corpus_stats`

```elixir
@spec corpus_stats(
  passing :: [String.t()],
  category :: category(),
  root :: String.t()
) :: stats()
```

How much of `category`'s emitted corpus `passing` covers.

`total` counts the test files on disk under the suite directory, which is the
only denominator the ratchet can reach 100% of - cases excluded at generation
time (`tools/corpus/*/exclusions.exs`) never emit a file. The numerator is the
intersection of `passing` with those files, never a plain length, because a
registry entry may be a glob or may name a path outside the suite directory.
`percent` is `nil` when the corpus is empty, so callers report "no files"
rather than dividing by zero.

# `default_path`

```elixir
@spec default_path() :: String.t()
```

Path of the registry file, relative to the project root.

# `encode`

```elixir
@spec encode(registry :: t()) :: String.t()
```

Encodes `registry` as JSON with sorted keys, two-space indent, and one array
entry per line, so that ratcheting in a test is a minimal diff.

## Examples

    iex> Mix.Statifier.RegressionRegistry.encode(%{"scion_tests" => ["a.exs"], "n" => 1})
    ~s|{\n  "n": 1,\n  "scion_tests": [\n    "a.exs"\n  ]\n}\n|

# `expand`

```elixir
@spec expand(registry :: t(), category :: category()) :: {[String.t()], [String.t()]}
```

Expands the patterns held under `category` into test files.

Returns `{files, missing}`, where `missing` lists entries that matched
nothing on disk. A registry entry that no longer exists is a hole in the
ratchet, so callers report it rather than skipping it.

# `expand_patterns`

```elixir
@spec expand_patterns(patterns :: [String.t()]) :: {[String.t()], [String.t()]}
```

Expands a list of literal paths and globs into existing test files.

Returns `{sorted_unique_files, patterns_that_matched_nothing}`.

## Examples

    iex> Mix.Statifier.RegressionRegistry.expand_patterns(["test/no_such_test.exs"])
    {[], ["test/no_such_test.exs"]}

# `files`

```elixir
@spec files(registry :: t()) :: {[String.t()], [String.t()]}
```

Every test file the registry expands to, across all categories.

Returns `{files, missing}` with the same meaning as `expand/2`.

# `key`

```elixir
@spec key(category :: category()) :: String.t()
```

The registry key holding `category`.

## Examples

    iex> Mix.Statifier.RegressionRegistry.key(:scion)
    "scion_tests"

# `load`

```elixir
@spec load(path :: String.t()) :: {:ok, t()} | {:error, String.t()}
```

Loads the registry from `path`.

Returns `{:error, reason}` rather than raising: both tasks report the reason
and exit non-zero, and a malformed registry must never be mistaken for an
empty one.

# `save`

```elixir
@spec save(registry :: t(), path :: String.t()) :: :ok | {:error, String.t()}
```

Writes `registry` back to `path`, pretty-printed.

# `stats_lines`

```elixir
@spec stats_lines(
  passing :: [String.t()],
  categories :: [category()],
  root :: String.t()
) :: [
  String.t()
]
```

One report line per conformance category, plus the denominator caveat.

Each per-category line reads `"  LABEL: passing/total (percent%)"`, with
labels padded so the counts line up. A category whose `total` is `0` is
omitted entirely. Callers prefix these lines with their own header (the
numerator's meaning differs between a scan and a ratchet run, so the header
text is not this function's job) and print the block as-is otherwise.
Returns `[]` - no lines, no caveat - when no category has any emitted files,
so a fixture tree with no corpus prints nothing at all.

# `suite_dir`

```elixir
@spec suite_dir(category :: category()) :: String.t() | nil
```

Directory holding the corpus for a conformance `category`.

## Examples

    iex> Mix.Statifier.RegressionRegistry.suite_dir(:w3c)
    "test/scxml_tests"

# `suite_label`

```elixir
@spec suite_label(category :: category()) :: String.t() | nil
```

Human-readable name of a conformance suite.

## Examples

    iex> Mix.Statifier.RegressionRegistry.suite_label(:scion)
    "SCION"

    iex> Mix.Statifier.RegressionRegistry.suite_label(:internal)
    nil

# `test_args`

```elixir
@spec test_args(paths :: [String.t()]) :: [String.t()]
```

`mix test` arguments that make `paths` runnable.

Conformance suites are excluded by default, so their tag has to be included
explicitly.

## Examples

    iex> Mix.Statifier.RegressionRegistry.test_args(["test/scion_tests/basic/basic0_test.exs"])
    ["--include", "scion"]

---

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