# `PhoenixKit.Migrations.Repair.Report`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.13.7/lib/phoenix_kit/migrations/repair/report.ex#L1)

The result shape `PhoenixKit.Migrations.Repair.verify/1` and `.repair/1` return —
versions observed, one finding per object/invariant that was not silently
clean, and the derived exit code for `mix phoenix_kit.repair` (spec §6.1's
final pipeline step, §6.2's severity mapping, and the `0`/`1`/`2` contract on
the mix task's moduledoc).

## Fields

  * `:prefix` — the schema this report is about.
  * `:dry_run` — whether the run that produced this report was read-only
    (`PhoenixKit.Migrations.Repair.verify/1`, or `.repair/1` with
    `dry_run: true`). Purely descriptive — every `finding.kind` already
    disambiguates `:would_repair` (dry-run) from `:repaired` (applied).
  * `:versions` — `%{comment: comment(), floor: pos_integer(), current: pos_integer()}`.
    `comment` is the **raw** value (see
    `PhoenixKit.Migrations.Repair.Probe.raw_comment/2`) — `:absent` (the
    `phoenix_kit` table does not exist), `nil` (table exists, no comment —
    the half-installed/adopt case), or the numeric value.
  * `:comment_action` — what, if anything, happened to the version comment
    this run: `:none`, `{:healed, version}` (stale-low, R2/`--heal-comment`),
    `{:adopted, version}` (R4/`--adopt`), or `{:would_heal, version}` /
    `{:would_adopt, version}` for a dry run that found the condition but
    did not write.
  * `:findings` — see `t:finding/0`.

## Severity → exit code

`mix phoenix_kit.repair`'s contract is exit `0`/`1`/`2` (moduledoc); this
module computes that from the **highest severity present**, never from
counting kinds one by one, so a run that both fixed something repairable
AND surfaced an error-severity divergence reports the divergence's exit
code (`2`) — the operator still needs to see it even though other things
got fixed:

  * any `severity: :error` finding present → `2` ("report-only divergences
    present" — spec §6.2's error-severity bucket: wrong type/length/
    default, unexpected NOT NULL, divergent index/constraint definition,
    `:create_failed`, a failing data invariant)
  * else any `severity: :repairable` finding present → `1` ("repairs
    applied-or-pending" — covers both an actual `:repaired` in `repair/1`
    and a `:would_repair` in `verify/1`/dry-run, deliberately collapsed to
    the same code per the mix task moduledoc's own wording)
  * else → `0` (clean; only `:info`-severity findings, if any)

# `comment`

```elixir
@type comment() :: :absent | nil | non_neg_integer()
```

Raw comment as read by `Probe.raw_comment/2` — never the legacy no-comment→1 mapping.

# `comment_action`

```elixir
@type comment_action() ::
  :none
  | {:healed, pos_integer()}
  | {:adopted, pos_integer()}
  | {:would_heal, pos_integer()}
  | {:would_adopt, pos_integer()}
```

# `finding`

```elixir
@type finding() :: %{
  kind: atom(),
  severity: severity(),
  object_id: String.t() | nil,
  since: pos_integer() | nil,
  message: String.t()
}
```

One reportable event. `since`/`object_id` are `nil` for findings that are not
about a single manifest object (comment-policy findings, data invariants use
`object_id: nil` + a `since` from the invariant).

# `severity`

```elixir
@type severity() :: :info | :repairable | :error
```

See the moduledoc's severity → exit code table.

# `t`

```elixir
@type t() :: %PhoenixKit.Migrations.Repair.Report{
  comment_action: comment_action(),
  dry_run: boolean(),
  findings: [finding()],
  prefix: String.t(),
  versions: %{comment: comment(), floor: pos_integer(), current: pos_integer()}
}
```

# `add_finding`

```elixir
@spec add_finding(t(), finding()) :: t()
```

Appends one finding (prepended internally; `findings/1` returns them in append order).

# `exit_code`

```elixir
@spec exit_code(t()) :: 0 | 1 | 2
```

The highest-severity-wins exit code — see the moduledoc's "Severity → exit
code" section. `0`/`1`/`2` only; never raises.

# `findings`

```elixir
@spec findings(t()) :: [finding()]
```

Findings in the order they were added.

# `new`

```elixir
@spec new(String.t(), boolean(), %{
  comment: comment(),
  floor: pos_integer(),
  current: pos_integer()
}) ::
  t()
```

Builds an empty report for the given prefix/dry_run/versions triple.

# `put_comment_action`

```elixir
@spec put_comment_action(t(), comment_action()) :: t()
```

Sets `comment_action`.

# `summary`

```elixir
@spec summary(t()) :: %{total: non_neg_integer(), by_severity: map(), by_kind: map()}
```

Counts findings by `severity` and by `kind`, plus the total — the numbers
`mix phoenix_kit.repair`'s human-readable summary line and `--json` output
both render from.

# `to_json_map`

```elixir
@spec to_json_map(t()) :: map()
```

Plain-data rendering for `--json` — every value is already a JSON scalar,
list, or map except `:versions.comment` (`:absent` needs a string form) and
`:comment_action` (a tagged tuple). Both are flattened here; everything else
(`finding.kind`/`finding.severity` atoms included) is left for the caller's
JSON encoder to stringify, matching how `mix phoenix_kit.doctor`/
`release_check` already emit atoms as plain output.

---

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