# `Mob.PostMortem.BeamCrashDump`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/post_mortem/beam_crash_dump.ex#L1)

Scan for `erl_crash.dump` files the BEAM leaves behind, and turn each into
a defect capsule.

## Why this exists

Every BEAM node that dies hard writes a crash dump — a text file at
`$ERL_CRASH_DUMP` if the env var is set, otherwise `erl_crash.dump` in the
runtime's cwd. Mob apps land it in the app data directory; host tooling
leaves them in the repo root. Nobody was collecting them until now: at
the time this shipped, five sibling repos on the author's machine each
had an unlooked-at dump in the root.

A crash the framework can prove happened is a defect it can report, and
the format from `decisions/2026-09-04-defect-reports-are-a-shipped-feature.md`
has a `:beam_crash` kind waiting for exactly this input.

## Discipline

* **Non-destructive.** The dump stays on disk. A future triager or the
  author might want the full file; deleting after emit would lose
  everything the capsule cannot fit.
* **Idempotent.** A sha256 of the file becomes the artifact id, and the
  `Mob.PostMortem.Registry` tracks emitted ids in a public ETS table. A
  re-sweep against the same file finds it seen and does nothing.
* **Bounded read.** A dump can be gigabytes on a busy scheduler. This
  module reads only the header window (default 8192) — that
  is where slogan, system version, taints and atoms count live. Anything
  beyond is by construction not on the emit path.
* **Fingerprint stripped of concrete data.** The slogan for a
  `{badarg, ...}` from `io:put_chars/2` embeds the raw binary that was
  passed. Fingerprinting that verbatim opens one triage row per unique
  input; stripping binaries and long numeric literals groups the whole
  class into one row. See `normalize_slogan/1`.

## Not in scope here

* The dump is not parsed for process state, ETS contents, or stack
  frames. A file the caller can open in `crashdump_viewer` is a better
  home for those. What ends up in the capsule is what a triager sees at
  a glance: which build, which OTP, what class of crash.
* iOS MetricKit and Android `ApplicationExitInfo` are separate
  substrates with their own modules (`Mob.PostMortem.IOS` /
  `Mob.PostMortem.Android`) and their own follow-up tickets.

# `finding`

```elixir
@type finding() :: %{
  path: String.t(),
  sha256: String.t(),
  size: non_neg_integer(),
  modified_at: DateTime.t() | nil,
  slogan: String.t() | nil,
  system_version: String.t() | nil,
  taints: [String.t()],
  atoms: non_neg_integer() | nil,
  dump_version: String.t() | nil
}
```

What a scanner returns for one dump file: enough to emit and to describe
what was scanned, without the raw bytes.

# `default_paths`

```elixir
@spec default_paths() :: [Path.t()]
```

Default paths a sweep looks at when the caller does not pass its own.

* The value of the `ERL_CRASH_DUMP` env var, if set (that is where the
  BEAM would have written this VM's own dump).
* The current working directory, where the BEAM writes by default.

A caller wanting a broader sweep supplies its own list.

# `emit`

```elixir
@spec emit([finding()]) :: [Mob.Defect.Capsule.t()]
```

Emit a capsule for every finding not already recorded in the registry.

Returns the capsules emitted, in the same order.

# `normalize_slogan`

```elixir
@spec normalize_slogan(String.t() | nil) :: String.t() | nil
```

A version of the slogan with concrete data stripped, for the fingerprint.

A raw slogan from a `{badarg, ...}` on `io:put_chars/2` looks like

    Runtime terminating during boot ({badarg,[{io,put_chars,[standard_error,[<<42,42,...4KB more...>>]]}]})

and the binary embedded there varies per crash. Two crashes for the same
root cause thus fingerprint differently, which is exactly the shattering
the bus is meant to prevent (see the "fingerprint and evidence are
separate" decision record).

Strip:

* Binary literals `<<...>>` — replaced by `<<>>`
* Long numeric sequences (>= 8 digits with commas or dots between them)
  that look like the printable form of the same — replaced by `<<>>`
* Runs of whitespace collapsed

What survives: atom-shaped tokens, module and function names, arities,
the structural punctuation. That is what identifies the *class* of
crash, which is what a fingerprint is for.

# `scan`

```elixir
@spec scan([Path.t()]) :: [finding()]
```

Scan `paths` for `erl_crash.dump` files.

`paths` is a list of file *or* directory paths. A directory is checked
only for a top-level `erl_crash.dump`; this module does not recurse.
Non-existent paths are silently skipped — a sweep from a caller that
overshoots the possibilities should not have to guard each one.

Returns the list of `t:finding/0` values.

Emission is a separate step: `emit/1` takes a list of findings and
puts anything the registry has not seen onto the defect bus. That
split is deliberate — a caller can list findings without publishing
them, and tests can assert on shape without touching the bus.

---

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