# `MobDev.OtpAudit.Slim`
[🔗](https://github.com/genericjam/mob_dev/blob/master/lib/mob_dev/otp_audit/slim.ex#L1)

In-place strip pass for the per-app OTP bundle, called by
`MobDev.NativeBuild` when `MOB_SLIM=1`. Was an inline ~100-line
`defp` in `native_build.ex`; extracted here so it's testable and
has a place for per-app override hooks to live.

## What gets stripped

Six phases run in fixed order against `<app_bundle>/otp`:

  1. `apple_binaries` — `.so` / `.a` and `priv/bin/*` (Apple-policy
     parity: no standalone executables in the bundle)
  2. `prefix_libs` — every `lib/<name>-*` whose `<name>` is in the
     computed strip set (see `compute_strip_set/1`)
  3. `foreign_apps` — `lib/{toy_,test_,mob_test,scratch_}*` (other
     projects' code that snuck into a shared OTP cache)
  4. `dedup_versions` — when the same lib appears at multiple
     versions, keep only the highest
  5. `src_and_headers` — every `src/` and `include/` directory
  6. `beam_chunks` — `:beam_lib.strip_release/1` drops Debug/Doc
     chunks from every `.beam`

Steps are intentionally idempotent so repeat runs are safe.

## Strip set composition

The default strip set is the hardcoded baseline (`hardcoded_prefixes/0`)
— a curated list of OTP libs mobile apps never need (megaco, snmp,
diameter, …). Per-app overrides in `mob.exs` adjust the set:

    config :mob_dev,
      slim: [
        drop_libs: ["foo_dep"],   # force-strip these too
        keep_libs: ["mnesia"]     # don't strip these even if baseline says so
      ]

`drop_libs` and `keep_libs` accept plain `<name>` strings — the
same shape `MobDev.OtpAudit`'s report uses, so users can copy
basenames directly out of `mix mob.audit_otp` output.

## Audit-driven expansion + the always-keep guardrail

When the caller passes `:audit_input` (typically via mob.exs
`slim: [audit: true, trace_json: "..."]`), the strip set grows
by:

  * `audit.foreign_app_names` (allow-list-validated cache cruft)
  * `strippable_libs ∩ trace_strippable_libs` (both signals agree)
  * `trace_strippable_libs \ strippable_libs` (trace-only — libs
    the static graph reaches but the trace says are never called)

The last category is the powerful one and the dangerous one. A
60-second trace window can easily miss libs that ARE used but
only at boot (`sasl`) or only in code paths the user didn't
drive during the window (`crypto`, `public_key`, `asn1`, `ssl`
when no TLS happens). Auto-stripping those would break apps.

The `always_keep_libs/0` set is the safety guardrail: those libs
are NEVER added to the strip set by audit-driven expansion, no
matter what the trace says. The hardcoded baseline still strips
what it strips, and `:drop_libs` still works as the user-explicit
escape hatch — but the trace can't accidentally take down crypto
or sasl.

Override patterns from `mob.exs`:

    config :mob_dev,
      slim: [
        # Force-keep — user override beats everything else.
        # Use this if audit-driven strip is taking out a lib you
        # actually need at runtime.
        keep_libs: ["some_lib"],

        # Force-strip — user override beats the audit. Use this
        # to expand beyond what the trace alone would mark.
        drop_libs: ["my_unused_dep"]
      ]

Precedence (in order from least to most authoritative): hardcoded
baseline → audit-derived expansion (minus always_keep_libs) → minus
`:required_libs` (what the app's `.app` closure needs) → `:drop_libs` →
`:keep_libs`. The user's `:keep_libs` is always the last word.

# `slim_result`

```elixir
@type slim_result() :: %{
  steps: [step_info()],
  final_kb: non_neg_integer(),
  strip_set: [String.t()]
}
```

# `step_info`

```elixir
@type step_info() :: %{
  label: String.t(),
  before_kb: non_neg_integer(),
  after_kb: non_neg_integer()
}
```

# `always_keep_libs`

```elixir
@spec always_keep_libs() :: [String.t()]
```

Libs that audit-driven expansion is forbidden from auto-stripping.
Source of truth for the safety guardrail — see moduledoc.
Users can still force-strip these via `:drop_libs` in `mob.exs`.

# `compute_strip_set`

```elixir
@spec compute_strip_set(keyword()) :: [String.t()]
```

Compute the final strip set. Returns a sorted, deduplicated list.

## Composition (low to high precedence)

  1. `hardcoded_prefixes/0` — baseline.
  2. `:audit_input` expansion (when given), minus `always_keep_libs/0`
     (the safety guardrail).
  3. `:required_libs` — subtracted: libs the app needs at runtime.
  4. `:drop_libs` — user-explicit force-strip.
  5. `:keep_libs` — user-explicit force-keep. Wins over everything.

## Recognized opts

  * `:keep_libs` — `[String.t()]`, force-keep (subtracts from set).
    Highest precedence — overrides every other source.
  * `:drop_libs` — `[String.t()]`, force-strip (adds to set).
    Higher precedence than the guardrail: a user can `:drop_libs`
    a lib that's in `always_keep_libs/0` if they really mean it.
  * `:required_libs` — names the app's runtime dependency closure
    needs (`MobDev.OtpRequiredApps.for_project/1`). Never stripped by
    the baseline or the audit: starting an app whose `.app` lists a
    missing lib fails on the device.
  * `:audit_input` — `MobDev.OtpAudit.report/0` to expand the strip
    set with audit-derived libs:
      - `report.foreign_app_names` always unions in (allow-list-
        validated by OtpAudit's `:project_deps`).
      - When the audit was run with `:trace_input`:
        - `strippable_libs ∩ trace_strippable_libs` unions in
          (both signals agree → high confidence).
        - `trace_strippable_libs \ strippable_libs` unions in
          (trace-only — the megaco/snmp/diameter unblocking signal).
      - `strippable_libs` alone is NOT unioned without trace
        (NIF dispatch like `exqlite` is statically invisible).
      - The expansion is then filtered through
        `always_keep_libs/0` so trace lies (e.g. trace window
        missed sasl boot or crypto's lazy TLS calls) can't break
        production.

# `hardcoded_prefixes`

```elixir
@spec hardcoded_prefixes() :: [String.t()]
```

Hardcoded baseline of OTP libs mobile apps never need. Source of
truth for what `mix mob.deploy --slim` strips by default.

# `slim_bundle`

```elixir
@spec slim_bundle(
  Path.t(),
  keyword()
) :: {:ok, slim_result()}
```

Apply the strip pass to an OTP bundle in place.

Recognized opts:
  * `:keep_libs`, `:drop_libs`, `:required_libs`, `:audit_input` — see
    `compute_strip_set/1`.
  * `:strip_set` — short-circuit override. When set, every other
    opt that would feed into the computation is ignored. Primarily
    for tests.
  * `:on_step` — optional callback `fn(step_info) -> any()` invoked
    after each phase. Caller uses it for build-log output.

Returns `{:ok, slim_result()}`. The bundle is mutated in place; if a
phase fails, the bundle is in a partially-stripped state (matches the
pre-extraction behaviour, which had no rollback either).

---

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