# `Mob.Defect`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/defect.ex#L1)

Entry points that convert what the framework's detectors produce into
capsules on the defect bus.

The framework's detectors have their own shapes — an invariant violation is
a `Mob.Invariant.Violation`, a divergence is the `{:divergence, %{...}}` a
comparator returns. Rather than teach each detector to build a capsule,
those detectors call the appropriate function here and this module supplies
the fingerprint key, the owner, and the evidence shape.

Centralising the mapping is what keeps `owner: :mob` from being copied into
five places, and what makes the fingerprint key for the same kind of defect
identical across detectors — the whole point of a fingerprint is that a
divergence detected on iOS from mob_dev and the same divergence detected
on-device from a fallback path land on the same triage item.

## Nothing propagates on the return path

Every function here returns the capsule it built, but the emit is a
side-effect on the bus and the caller does not need to do anything with
the return value. That matches the shape of `Mob.Agent.Receipts.record/1`
and lets a caller drop the emit onto an existing pipeline without
restructuring around a result.

# `emit_appexit_reason`

```elixir
@spec emit_appexit_reason(map()) :: Mob.Defect.Capsule.t()
```

Emit a defect for one Android `ApplicationExitInfo` entry.

Owner is `:mob` — an ApplicationExitInfo record describes the death
of this app's own process from the OS's point of view, and mob owns
the process during a mob-app lifetime.

The `entry` map comes from `Mob.PostMortem.Android.sweep/0`'s drain
of the native queue. Its shape:

    %{
      reason_code: 6,       # ApplicationExitInfo.REASON_* integer
      pid: 12345,
      timestamp_ms: 1_726_050_000_000,
      process_name: "com.example.app",
      description: "remote process crash"
    }

Reason code → kind mapping (numeric constants from Android's
`android.app.ApplicationExitInfo` public API, hard-coded here rather
than depending on the JNI shim to name them):

| Reason(s) | `kind` | `severity` |
|---|---|---|
| `REASON_CRASH_NATIVE` (5), `REASON_CRASH` (4), `REASON_SIGNALED` (2) | `:native_crash` | `:fatal` |
| `REASON_ANR` (6) | `:anr` | `:critical` |
| `REASON_LOW_MEMORY` (3), `REASON_EXCESSIVE_RESOURCE_USAGE` (9) | `:oom` | `:fatal` |
| `REASON_USER_STOPPED` (11), `REASON_USER_REQUESTED` (10), `REASON_EXIT_SELF` (1), `REASON_DEPENDENCY_DIED` (12) | `:user_kill` | `:info` |
| anything else (unknown / rare) | `:user_kill` | `:info` |

The `:info` default for unrecognised reason codes is deliberate. A
novel reason a future Android version invents is not a defect to
page a triager on; downgrading to routine is safer than upgrading
to critical. The specific `REASON_*` mappings above match Android's
documented severity semantics.

Fingerprint groups by `(kind + process_name + reason_code)` — the
same class of exit for the same process across boots becomes one
triage row.

# `emit_beam_crash`

```elixir
@spec emit_beam_crash(map()) :: Mob.Defect.Capsule.t()
```

Emit a defect for an `erl_crash.dump` a scan turned up.

Owner is `:mob` — the BEAM is what mob depends on and what mob's own
invariants and scheduling assume; a hard crash there is a framework
concern before it is an application one. `kind: :beam_crash`.

Severity comes from what the slogan implies: OOM and out-of-memory
variants are `:fatal`; a normal termination is `:info`; everything
else defaults to `:critical`.

The fingerprint key is the *normalized* slogan (see
`Mob.PostMortem.BeamCrashDump.normalize_slogan/1`) rather than the raw
one, because raw slogans embed the exact runtime data that varied per
crash — a `{badarg, ...}` from `io:put_chars/2` carries a 4KB binary
literal that would otherwise open one triage row per unique input.

Evidence carries the human-facing fields plus the on-disk path so a
triager can open the dump in `crashdump_viewer` when they want more
than the header. The dump file itself is NOT read into evidence —
eight kilobytes is the ceiling and the header is what fits.

# `emit_divergence`

```elixir
@spec emit_divergence(map(), keyword() | map()) :: Mob.Defect.Capsule.t()
```

Emit a defect for a differential-comparator divergence.

Real entrypoint, pending an in-tree caller. `Mob.Differential.compare/3`
is a pure comparator; the caller that runs it — currently `mob_dev`, via
`MobDev.Differential.run/3` — invokes this after a `:divergence` result to
put the class onto the bus. A follow-up mob_dev change wires that up
after this ships to Hex; nothing in `mob` itself calls this yet.

Owner is `:mob` — the differential comparator asserts one design, both
platforms, which is a framework-level promise. `kind: :divergence`.

The fingerprint key includes the fixture (so the same divergence in two
fixtures reports separately), the `reason` (`:type`, `:label`, `:frame`,
`:child_count`), and the `path` to the diverging node. The `ios` and
`android` values are on evidence, not on the fingerprint key — a fixture's
divergence in kind (say, always `:type` at `[0, 2]`) is the same class
even if the tree values change slightly across releases.

# `emit_invariant_violation`

```elixir
@spec emit_invariant_violation(Mob.Invariant.Violation.t()) :: Mob.Defect.Capsule.t()
```

Emit a defect for a confirmed invariant violation.

Owner is `:mob` — invariants are checks the framework makes about itself,
and by construction they cannot fail for the app. `kind: :invariant`.

The fingerprint key is `%{invariant: name, screen: screen_module}`, so a
violation of the same invariant on the same screen groups across
occurrences, and the same invariant on a different screen — a leak class
that appears in two places — reports separately.

# `emit_metrickit_payload`

```elixir
@spec emit_metrickit_payload(map()) :: Mob.Defect.Capsule.t()
```

Emit a defect for one MetricKit diagnostic delivered on iOS.

Owner is `:mob` — a MetricKit payload names an event the OS
attributes to this app process, and mob owns the process during a
mob-app lifetime.

The `payload` map comes from `Mob.PostMortem.IOS.sweep/0`'s drain of
the native queue. Its shape:

    %{
      kind: :native_crash | :anr | :perf_regression,
      top_frame: %{binary: "MyApp", offset: 123456},
      timestamp_ms: 1_726_050_000_000,
      raw_json: <<...>>  # MXDiagnosticPayload.JSONRepresentation
    }

The fingerprint key is the top stack frame — a crash in the same
place across builds groups into one triage row. The frame's
`binary` name is not the human function name (MetricKit gives
mangled symbols only, no user data); it is the safe identifier the
OS gives us, which is what we can safely put on the wire.

Severity is picked from the diagnostic kind:
- `:native_crash` → `:fatal` (the app terminated abnormally)
- `:anr` → `:critical` (the main thread stalled past the OS's
  hang threshold; the app may or may not have recovered)
- `:perf_regression` → `:warning` (CPU / disk exception; the app
  kept running, this is a heads-up)

---

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