# `Excessibility.Review`
[🔗](https://github.com/lessthanseventy/excessibility/blob/v0.18.1/lib/excessibility/review.ex#L1)

A blast-radius report for a snapshot run, measured against the baseline.

Where `mix excessibility` checks each snapshot in isolation, a review
asks a sharper question: *what did this change actually do?* It diffs
every current snapshot against its baseline (the known-good/`main`
state) and, per view, reports:

  * the rendered regions that changed (via `Excessibility.SnapshotDiff`)
  * the accessibility findings this change **newly introduced** —
    axe-core violations and `Excessibility.LiveViewRules` violations
    present now but not in the baseline (a finding-delta)
  * a risk **tier** — `:auto`, `:review`, or `:block`

With `content_diff: true`, content that changed without an `aria-live`
announcement is also flagged. That signal compares rendered text, so it
is only meaningful when the baseline and current snapshots rendered the
**same fixture data** — with the usual CI shape (baseline and current
from two independent `mix test` runs) it mostly reports fixture drift,
which is why it is off by default.

axe-core runs through the configured `:scanner_mod` (a browser scan of
each side of the pair); disable it with `axe: false`. When a scan fails
(e.g. Playwright isn't installed) the review still runs on the LiveView
rules alone and says so in the report's `:warnings`.

The tier is a transparent heuristic over the new findings; a smarter
judge can be layered on top of the same report. Because it diffs against
the baseline rather than two git refs, it runs from an ordinary
`mix test` + baseline, with no worktree gymnastics.

# `change`

```elixir
@type change() :: %{
  view: String.t(),
  regions: [Excessibility.SnapshotDiff.region()],
  region_count: non_neg_integer(),
  findings: [map()],
  behavioral: [Excessibility.Review.Behavioral.finding()],
  warnings: [String.t()],
  tier: tier()
}
```

# `report`

```elixir
@type report() :: %{
  changes: [change()],
  behavioral: [Excessibility.Review.Behavioral.finding()],
  warnings: [String.t()],
  summary: %{
    auto: non_neg_integer(),
    review: non_neg_integer(),
    block: non_neg_integer()
  }
}
```

# `tier`

```elixir
@type tier() :: :auto | :review | :block
```

# `judge_changes`

```elixir
@spec judge_changes(
  report(),
  keyword()
) :: report()
```

Run the configured judge over each change in a report.

Attaches the judge's verdict to every change, replaces the change's
tier with the judged tier, and recomputes the summary. Run-level
behavioral findings are handed to the judge as context
(`:run_behavioral`) but are **not** attributed to any view — they
stay at the report level, where `mix excessibility.review` prints
them and gates the exit code on them. A view's tier only reflects
what that view introduced.

# `review`

```elixir
@spec review(keyword()) :: report()
```

Review every current snapshot that has a baseline, returning a report.

Reads snapshots from the configured output path (`html_snapshots/` vs
`baseline/`). Only views that actually changed (regions or findings)
appear in `:changes`.

# `review_pair`

```elixir
@spec review_pair(String.t(), String.t(), String.t(), keyword()) :: change()
```

Review a single view's baseline-vs-current snapshot pair.

# `review_pairs`

```elixir
@spec review_pairs(
  [{String.t(), String.t(), String.t()}],
  keyword()
) :: report()
```

Review a list of `{view, baseline_html, current_html}` tuples.

Unchanged views (no regions and no findings) are dropped. Returns a
report with per-view changes and a tier summary.

# `tier`

```elixir
@spec tier([map()]) :: tier()
```

The risk tier for a list of findings (accessibility and/or behavioral).

Driven by the worst severity: a new critical/serious finding is `:block`,
any other finding is `:review`, none is `:auto`. Regions are reported for
context but don't escalate on their own.

---

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