# `PhoenixKit.Migrations.Modules`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.6.0/lib/phoenix_kit/migrations/modules.ex#L1)

Discovers the schema version of every installed PhoenixKit module that owns
its migrations.

Modules that ship their own tables implement `c:PhoenixKit.Module.migration_module/0`,
returning a coordinator with `current_version/0` (what the code needs) and
`migrated_version_runtime/1` (what the database has). Examples in the wild:
`phoenix_kit_inbox`, `phoenix_kit_boards`, `phoenix_kit_web_analytics`,
`phoenix_kit_legal`, `phoenix_kit_stats`.

This module is the shared read side of that contract. `mix phoenix_kit.status`
uses it to *report* per-module versions and `mix phoenix_kit.update` uses it
to decide what to *migrate* — before this existed each task had its own copy
of the discovery logic, and only `update` had it at all, which is why
`status` never mentioned modules.

Everything here is read-only and defensive: a module whose coordinator raises,
exits, or reports a non-integer version is recorded as `:error` with the
message, never crashing the caller. A broken third-party module must not take
down `mix phoenix_kit.status`.

## Example

    iex> PhoenixKit.Migrations.Modules.list()
    [
      %{
        name: "Inbox",
        module: PhoenixKitInbox,
        migration_module: PhoenixKitInbox.Migrations,
        installed: 1,
        target: 1,
        status: :up_to_date,
        error: nil
      }
    ]

# `entry`

```elixir
@type entry() :: %{
  name: String.t(),
  module: module(),
  migration_module: module(),
  installed: non_neg_integer(),
  target: non_neg_integer() | nil,
  status: status(),
  error: String.t() | nil
}
```

# `status`

```elixir
@type status() :: :not_installed | :needs_update | :up_to_date | :error
```

Where one module's schema stands.

  * `:not_installed` — the code is present but its tables have never been
    created (installed version 0). The next `mix phoenix_kit.update` creates
    them.
  * `:needs_update` — tables exist at an older version than the code expects.
  * `:up_to_date` — database matches (or exceeds) what the code needs.
  * `:error` — the module's coordinator raised, exited, or reported a
    non-integer version; the `:error` field has the message.

# `classify`

```elixir
@spec classify(term(), term()) :: status()
```

Classifies one module's installed version against the version its code wants.

Public because it is the whole read-side decision — `mix phoenix_kit.status`
renders it and `mix phoenix_kit.update` migrates off it — and a private
version could only be tested through a hand-built entry that supplied the
answer, which is no test at all.

Both versions must be integers. A coordinator that reports anything else
(`nil` for "no version comment found" is the tempting one) is `:error`, never
silently `:up_to_date`: under Erlang term ordering `nil >= 2` is `true`, so an
unguarded comparison would mark a module with no tables as current and skip
its migration forever.

    iex> alias PhoenixKit.Migrations.Modules
    iex> {Modules.classify(2, 2), Modules.classify(3, 2)}
    {:up_to_date, :up_to_date}
    iex> {Modules.classify(0, 1), Modules.classify(1, 5)}
    {:not_installed, :needs_update}
    iex> Modules.classify(nil, 2)
    :error

# `failed`

```elixir
@spec failed([entry()]) :: [entry()]
```

Filters a `list/1` result down to entries whose coordinator raised. Surfaced
separately so tasks can warn about a broken module instead of quietly
omitting it from the report.

Pure filter, same reasoning as `pending/1`.

# `list`

```elixir
@spec list(keyword()) :: [entry()]
```

Every discovered module that owns migrations, sorted by display name.

Returns `[]` when module discovery itself fails (no beam files, app not
loaded) rather than raising — callers are CLI tasks that should degrade to
"no modules" instead of blowing up.

## Options

  * `:prefix` — Postgres schema the install lives in. Defaults to `"public"`.

# `pending`

```elixir
@spec pending([entry()]) :: [entry()]
```

Filters a `list/1` result down to the entries a `mix phoenix_kit.update` run
would act on — those needing their tables created or upgraded.

A pure filter, deliberately: an earlier version also accepted options and did
its own `list/1`, which made `pending([])` ambiguous — an empty *entry list*
and empty *options* are the same term, so "nothing to filter" silently became
"go query the database". Callers that want both compose them:

    Modules.list(prefix: prefix) |> Modules.pending()

---

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