# `ExQuality.Stage`
[🔗](https://github.com/riddler/ex_quality/blob/v0.15.0/lib/ex_quality/stage.ex#L1)

Type definitions for quality check stage results.

Each stage returns a result map with standardized fields for
status, output, stats, and timing information.

## Findings

A stage may also return `findings`, a list of `ExQuality.Finding` structs
parsed from its tool's output. Findings are optional: a stage that has no
parser, or whose output did not parse this run, simply omits the key.

Renderers follow one rule:

1. If `findings` is non-empty, render the findings.
2. Otherwise, print `output` verbatim. Unparseable output is never hidden.

## Metadata

A stage may also return `meta`, a map of extra report fields describing *what
the stage did* rather than what it found. The test stage uses it to say how
much of the suite it ran, because `"status": "ok"` over three test files and
`"status": "ok"` over the whole suite are different claims and a consumer has
to be able to tell them apart. Keys are merged into the stage's object in the
JSON report.

## Skipped stages

A stage that was considered and not run returns a `:skipped` result carrying
the reason in `summary`, built with `skipped/3`. A run that says nothing about
a stage is indistinguishable from a run where the stage had nothing to say,
so silence is never an option.

A skip has a kind as well as a reason, because the two kinds mean opposite
things to whoever reads the run afterwards. `:run` means the caller asked for
a narrower run - `--quick`, a profile, `--until-first-failure` - and a full
`mix quality` closes the gap. `:project` means the project does not check
this at all - the tool is not installed, the stage is disabled in
`.quality.exs` - and a fuller run cannot close it. The kind is structural
rather than read out of the reason's wording, so a consumer such as
`mix quality.verify` never has to substring-match prose that a patch release
is free to rephrase.

# `kind`

```elixir
@type kind() :: :reader | :writer
```

Whether a stage reads the build or writes to it.

The analysis phase runs its stages concurrently, which is only safe while
every one of them is a reader. A stage that recompiles the project or
rewrites files under `_build` invalidates the beams the others are part-way
through reading, and the reader that notices reports a failure that has
nothing to do with the code. Writers are run on their own, before the
readers, for the same reason compilation is a serialized gate.

A stage that does not say is a `:reader`.

# `result`

```elixir
@type result() :: %{
  :name =&gt; String.t(),
  :status =&gt; :ok | :error | :skipped,
  :output =&gt; String.t(),
  :stats =&gt; stats(),
  :summary =&gt; String.t(),
  :duration_ms =&gt; non_neg_integer(),
  optional(:skip_kind) =&gt; skip_kind(),
  optional(:findings) =&gt; [ExQuality.Finding.t()],
  optional(:meta) =&gt; %{required(atom()) =&gt; term()}
}
```

# `skip_kind`

```elixir
@type skip_kind() :: :run | :project
```

What a `:skipped` result means to whoever reads the run afterwards.

`:run` names this run: the caller asked for a narrower one, and a full
`mix quality` closes the gap. `:project` names the project: it does not
check this at all, and a fuller run cannot close it.

# `stats`

```elixir
@type stats() :: %{
  optional(:test_count) =&gt; non_neg_integer(),
  optional(:passed_count) =&gt; non_neg_integer(),
  optional(:failed_count) =&gt; non_neg_integer(),
  optional(:failures_by_app) =&gt; [{String.t(), non_neg_integer()}],
  optional(:coverage) =&gt; float(),
  optional(:coverage_by_app) =&gt; [{String.t(), float()}],
  optional(:coverage_required) =&gt; number(),
  optional(:warning_count) =&gt; non_neg_integer(),
  optional(:plt_built) =&gt; boolean(),
  optional(:issue_count) =&gt; non_neg_integer(),
  optional(:unused_deps) =&gt; non_neg_integer(),
  optional(:vulnerabilities) =&gt; non_neg_integer(),
  optional(:vulnerabilities_by_severity) =&gt; [{String.t(), non_neg_integer()}],
  optional(:files_formatted) =&gt; non_neg_integer(),
  optional(:files_needing_format) =&gt; non_neg_integer(),
  optional(:missing_translations) =&gt; non_neg_integer(),
  optional(:fuzzy_translations) =&gt; non_neg_integer(),
  optional(:file_count) =&gt; non_neg_integer(),
  optional(:finding_count) =&gt; non_neg_integer(),
  optional(:link_count) =&gt; non_neg_integer(),
  optional(:blocking_count) =&gt; non_neg_integer(),
  optional(:informational_count) =&gt; non_neg_integer(),
  optional(:blocking_by_confidence) =&gt; [{String.t(), non_neg_integer()}],
  optional(String.t()) =&gt; term()
}
```

# `findings`

```elixir
@spec findings(map()) :: [ExQuality.Finding.t()]
```

Returns a result's findings, or an empty list when the stage reported none.

    iex> ExQuality.Stage.findings(%{name: "Credo"})
    []

# `kind`

```elixir
@spec kind(
  module(),
  keyword()
) :: kind()
```

Returns whether a stage module reads the build or writes to it.

A module says so by exporting `stage_kind/1`, which is given the run's
config because a stage can be a writer only in some configurations. A module
that does not export it is a `:reader`, so classifying a stage is one
function on the stage that has something to declare rather than a line on
every stage that does not.

    iex> ExQuality.Stage.kind(ExQuality.Stages.Credo, [])
    :reader

    iex> ExQuality.Stage.kind(ExQuality.Stages.Gettext, gettext: [extract: true])
    :writer

# `skipped`

```elixir
@spec skipped(String.t(), String.t(), skip_kind()) :: result()
```

Builds a `:skipped` result for a stage that was considered and not run.

The reason is carried in `summary` so renderers can say why the stage did
not run rather than leaving a gap in the output. The kind says which of the
two things the skip means - see `t:skip_kind/0`.

The default kind is `:project`, because that is the conservative direction:
an unlabelled skip fails to attest as a standing gap rather than passing as
a narrowing nobody declared. Every built-in call site passes the kind; a
custom stage that skips itself should too.

    iex> ExQuality.Stage.skipped("Dialyzer", "--quick", :run)
    %{
      name: "Dialyzer",
      status: :skipped,
      output: "",
      stats: %{},
      summary: "--quick",
      duration_ms: 0,
      skip_kind: :run
    }

    iex> ExQuality.Stage.skipped("Sobelow", ":sobelow not installed").skip_kind
    :project

---

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