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

Writes the language-neutral conformance corpus under `conformance/`, and
checks the committed one (ADR-0070). `mix statifier.corpus` is its command
line.

## Emit

`emit/1` reads the fetched and transformed upstream suites
(`Mix.Statifier.Corpus.Upstream`) with this repository's exclusion lists
and the W3C sub-document set (`Mix.Statifier.Corpus.Exclusions`), and the
`statifier` cases this repository authors under `conformance/cases/`
(`Mix.Statifier.Corpus.Authored`), RUNS every case through statifier
(`Mix.Statifier.Corpus.Runner`), and only then writes:

  * `conformance/corpus/<suite>.json` - one file per suite that has cases,
    the cases sorted by id, one case per line. A suite with no cases gets
    no file and no manifest entry, so no empty corpus file is ever written.
  * `conformance/manifest.json` - the `corpus_hash`, one entry per corpus
    file with its case count, and the upstream suites with their licences.
    It carries no statifier-ex version: a claim is pinned by the
    `corpus_hash` and the statifier-ex tag the corpus was vendored from
    (ADR-0070 decision 4), so a version bump changes nothing it holds.
  * `conformance/exclusions.json` - the exclusion lists, each entry with its
    reason atom, its prose and, where the prose cites a decision record,
    that record's number as `adr`. A SCION directory key stays one entry
    naming the directory: it is not expanded into the upstream cases under
    it, so the file does not depend on the fetched tree.
  * `conformance/registry.json` - statifier-ex's claim against the corpus,
    derived from the ratchet's SCION and W3C lists by
    `Mix.Statifier.Corpus.Registry` and pinned by the `corpus_hash`. A
    ratchet path that names no corpus case, or a ratchet that names none,
    stops the emit and nothing is written.

A case's configurations are the upstream's expectation, and a case that
the regression ratchet (`test/passing_tests.json`) lists must agree with
it when run: one that disagrees stops the emit and nothing is written. A
case outside the ratchet is written with the upstream expectation whatever
its run found; its absence from the ratchet is what says statifier-ex does
not claim it, and the emit reports each such case with its outcome. An
authored `statifier` case has no upstream expectation to defer to: its
expectation is what this repository wrote, so one that disagrees when run
stops the emit as a ratcheted case does.

`corpus_hash` is `"sha256:"` followed by the lowercase hex SHA-256 of the
corpus files' bytes concatenated in suite order - `scion`, `w3c`,
`statifier` - skipping a suite with no file. Nothing written depends on the
time or on a filesystem path, so two emits of one tree are byte-identical.

The licence texts under `conformance/LICENSES/` are committed copies of the
upstream licences, not generated; `emit/1` and `check/1` refuse when a
notice a case points at is missing.

## Check

`check/1` writes nothing and needs neither the network nor the upstream
tree. From the committed files alone it re-runs every committed case from
its committed source, recomputes every file derivable from committed
inputs - each corpus file's canonical form, `corpus/statifier.json` from
the authored cases under `conformance/cases/`, each case's
`required_features`, the manifest with its `corpus_hash`,
`exclusions.json`, and the registry derived from `test/passing_tests.json` -
and fails on any difference, on a committed corpus file the inputs no
longer derive, on a registry with no entries or with an entry naming a
case the corpus lacks or holds under another suite, and on any ratcheted
or authored case whose run disagrees. When the upstream tree is present it
also rebuilds the corpus from it and fails on any case that differs; when
it is absent it says that comparison was skipped. A check that finds a
corpus file missing or empty, or that visited no case, fails: a green
result on nothing is a defect.

# `config`

```elixir
@type config() :: %{root: Path.t(), scratch: Path.t()}
```

Where the emitter reads and writes: the project root and the upstream tree.

# `report`

```elixir
@type report() :: %{
  :counts =&gt; [{String.t(), non_neg_integer(), non_neg_integer()}],
  :outside_ratchet =&gt; [{String.t(), Mix.Statifier.Corpus.Runner.outcome()}],
  optional(:claims) =&gt; [{String.t(), pos_integer()}],
  optional(:written) =&gt; [Path.t()],
  optional(:upstream) =&gt; :compared | {:skipped, Path.t()}
}
```

What an emit or a check found, for the task to print.

# `check`

```elixir
@spec check(config :: config()) :: {:ok, report()} | {:error, String.t()}
```

Checks the committed corpus, manifest, exclusions and registry without
writing anything, as the moduledoc describes.

# `config`

```elixir
@spec config(opts :: keyword()) :: config()
```

Builds a config from `opts`: `:root` (default `"."`) and `:scratch`
(default `tools/corpus/scratch` under the root).

## Examples

    iex> Mix.Statifier.Corpus.Emitter.config([])
    %{root: ".", scratch: "./tools/corpus/scratch"}

# `corpus_hash`

```elixir
@spec corpus_hash(contents :: [binary()]) :: String.t()
```

The `corpus_hash` of corpus file contents given in suite order.

## Examples

    iex> Mix.Statifier.Corpus.Emitter.corpus_hash(["a", "b"])
    "sha256:fb8e20fc2e4c3f248c60c39bd652f3c1347298bb977b8b4d5903b85055620603"

# `emit`

```elixir
@spec emit(config :: config()) :: {:ok, report()} | {:error, String.t()}
```

Reads the upstream tree, runs every case, and writes the corpus, the
manifest, the exclusions and the registry under `conformance/`. Writes
nothing when any step refuses.

# `generated_path`

```elixir
@spec generated_path(corpus_case :: map(), root :: Path.t()) :: Path.t() | nil
```

The path, relative to the project root, of the generated test module a
corpus case corresponds to - the path `test/passing_tests.json` names it by.
A `statifier` case has none.

# `render`

```elixir
@spec render(
  cases :: [map()],
  exclusions :: [Mix.Statifier.Corpus.Exclusions.entry()]
) :: %{
  required(Path.t()) =&gt; binary()
}
```

Renders `cases` and `exclusions` as the files `emit/1` writes, keyed by
path relative to `conformance/`.

---

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