# `MobDev.Plugin.Validator`
[🔗](https://github.com/genericjam/mob_dev/blob/master/lib/mob_dev/plugin/validator.ex#L1)

Validates plugin manifests, in two stages (see `MOB_PLUGINS.md`).

**Single-plugin** (`validate_plugin/3`, behind `mix mob.validate_plugin`): a
plugin author's pre-publish check — required fields, referenced files exist,
`mob_version` satisfied by the installed mob, plus advisory warnings.

**Cross-plugin** (`cross_validate/1`, run by mob_dev when activating): the
collision checks that only make sense across the *set* of activated plugins —
no two may claim the same component atom, screen route, or migration namespace.

Every result is `%{errors: [...], warnings: [...]}`. Errors fail loud;
warnings are advisory. Both stages are pure given their inputs (the only I/O
is `File.exists?/1` for path checks, isolated in `validate_plugin/3`).

# `result`

```elixir
@type result() :: %{errors: [String.t()], warnings: [String.t()]}
```

# `activated_capability_errors`

```elixir
@spec activated_capability_errors([{Path.t(), map() | nil}]) :: [String.t()]
```

Activation-time capability check across every activated plugin.

`plugins` is the `MobDev.Plugin.activated/0` shape — a list of
`{plugin_dir, manifest}` pairs. For each plugin, runs
`validate_swift_imports/2` and `validate_android_permissions/2` and
returns the flattened error list, each entry prefixed with the
plugin's `:name` so the user can tell which plugin tripped.

Empty list means every activated plugin's source matches its manifest's
declared capability surface. The build hooks this into iOS + Android
builds via `raise_on_capability_drift!/1`, which raises a `Mix.raise/1`
with the full list when any drift is found.

# `conflict_surface`

```elixir
@spec conflict_surface() :: %{required(atom()) =&gt; tuple()}
```

The cross-plugin **conflict surface**: every `MobDev.Plugin.Merge` gatherer
(each combines N plugins' manifest contributions into one space) classified by
what happens when two plugins clash. The map is keyed by the Merge gatherer
name, and `conflict_surface_test` asserts it covers **every** public gatherer —
so a new shared-resource field can't be added without classifying its conflict
behavior here (the systematic guarantee that multiples compose safely).

Kinds:
  * `{:collision, [{label, extractor}]}` — two plugins contributing the same
    value is a build error. `extractor` returns the values one manifest claims.
  * `{:namespaced, reason}` — per-plugin namespaced; no cross-plugin collision.
  * `{:union, reason}` — set-union semantics; duplicates are harmless.
  * `{:build_time, reason}` — collision is caught later in the native build
    (e.g. font/migration planners in `MobDev.Plugin.Assets`), not here.
  * `{:derived, reason}` — returns values derived from another classified field;
    introduces no new namespace of its own.

# `cross_validate`

```elixir
@spec cross_validate([{atom(), map() | nil}]) :: result()
```

Cross-plugin collision validation across the activated set.

`plugins` is a list of `{name, manifest}` for the activated plugins (tier-0
no-manifest plugins, i.e. `manifest == nil`, contribute nothing and are
ignored).

# `raise_on_capability_drift!`

```elixir
@spec raise_on_capability_drift!([{Path.t(), map() | nil}]) :: :ok
```

Runs `activated_capability_errors/1` and raises a `Mix.raise/1` (with a
user-readable bullet list) when any plugin's source references a
capability not in its manifest. No-op when every plugin is clean.

Lives in the validator (not NativeBuild) so the iOS-sim and iOS-device
build paths can both call it as a one-liner, and so it stays unit-testable.

Also runs the Phase 2 signature gate
(`MobDev.Plugin.SignatureGate.raise_on_signature_drift!/1`) at the top
— fails fast on a tampered or untrusted plugin before any capability
analysis runs. The unsigned-plugin banner is printed afterwards so it
surfaces on every successful invocation.

# `referenced_paths`

```elixir
@spec referenced_paths(map() | nil) :: [String.t()]
```

Collects the file paths a manifest references, relative to the plugin root.

Pure. Covers the concrete file declarations (`nifs.native_dir`,
`android.bridge_kt`, `android.jni_source`, `ios.swift_files`). Component
`view_module`/`composable` are type/function names, not paths, so they are
not included here.

# `validate_android_permissions`

```elixir
@spec validate_android_permissions(map() | nil, Path.t()) :: [String.t()]
```

Verifies every `<uses-permission android:name="X"/>` declared in
AndroidManifest.xml fragments under the plugin's tree appears in
`manifest.android.permissions`.

Scope (deliberate): scans `priv/native/android/**/*.xml` for
`<uses-permission/>` entries — declarations the plugin author explicitly
wrote. Does not attempt to infer permissions from Kotlin/Java API usage
(the static-analysis rabbit hole `MOB_PLUGIN_SECURITY.md` warns against).
Returns `[]` when the plugin ships no AndroidManifest fragment.

# `validate_plugin`

```elixir
@spec validate_plugin(map() | nil, Path.t(), String.t() | nil) :: result()
```

Single-plugin validation, run from the plugin's own project directory.

`installed_mob_version` is the version of `:mob` resolved in the plugin's
deps (a string), or `nil` to skip the compatibility check.

# `validate_swift_imports`

```elixir
@spec validate_swift_imports(map() | nil, Path.t()) :: [String.t()]
```

Verifies every `import X` in the plugin's Swift sources resolves to either
a base iOS framework or one declared in `manifest.ios.frameworks`.

See `MOB_PLUGIN_SECURITY.md` (Layer 2 — capability enforcement at compile
time): the manifest is the contract; the plugin's source cannot reach for a
framework that isn't manifest-declared. Catches drift at validate time
rather than at link time, where the error points at the linker invocation
and not at the manifest that produced it.

Returns a list of error strings (empty when the manifest passes). Skips
plugins with no `ios.swift_files`. Files referenced by the manifest but
missing on disk are flagged by `add_path_errors/3`, not here — this check
only opens files that exist.

---

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