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

# `combined_usage_descriptions`

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

The usage descriptions `Merge.plist_keys/1` combines from more than one
plugin, as `{key, plugin names}` sorted by key: keys at least two plugins
declare with different strings. Used to tell the host that its permission
prompt text was put together from several plugins and how to word it itself.

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

A privacy usage description (`Manifest.usage_description_key?/1`) that every
declaring plugin gives as a string is never a conflict either:
`Merge.plist_keys/1` combines the sentences, so no plugin's reason is lost
(`combined_usage_descriptions/1`).

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