# `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`](https://github.com/GenericJam/mob/blob/master/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() | Path.t(), map() | nil}],
  keyword()
) :: 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). The first element identifies the plugin in error messages: a
plugin directory (as `MobDev.Plugin.activated/0` returns) is reported as its
`priv/mob_plugin.exs` path, anything else via `inspect/1`.

Options:
  * `:host_plist_keys` — the keys the host's own `ios/Info.plist` sets. For a
    scalar value the host's wins over every plugin's (plugin keys only fill
    gaps), so two plugins declaring one of these keys is not a conflict:
    neither value lands. A key any plugin declares as an array still
    conflicts: arrays merge into the host's array instead of yielding to it.

# `host_plist_keys`

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

The top-level keys an Info.plist sets (`[]` when the file is absent or
unparseable, which keeps every collision an error). The XML is parsed
structurally (`MobDev.IosLayoutPlist.top_level_keys/1`); a binary plist is
converted with `plutil` first when it is available.

# `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.

# `raise_on_cross_plugin_conflicts!`

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

Runs `cross_validate/2` over the activated `{plugin_dir, manifest}` pairs and
`Mix.raise/1`s on any collision, so a native build stops before codegen
instead of letting one plugin's registration silently overwrite another's.
Info.plist keys the project's own `ios/Info.plist` already sets are exempt
(the project's value wins; see `cross_validate/2`).

# `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`](https://github.com/GenericJam/mob/blob/master/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`](https://github.com/GenericJam/mob/blob/master/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*
