# `PhoenixKit.Migrations.Repair.Scope`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.13.7/lib/phoenix_kit/migrations/repair/scope.ex#L1)

Spec §6.1's scope rule as pure functions: which manifest objects/invariants
a given comment version puts "in scope", and at which revision. Everything
here is DB-free.

## The scope rule, restated

> repair applies manifest objects with `since <= comment` only, each at the
> newest **revision** with `as_of_version <= comment` (never a future
> shape); objects with `since > comment` are pending — reported, never
> pre-applied.

`resolve/2` gets the second half right for every class by always selecting
through `PhoenixKit.Migrations.ExpectedSchema.Object.shape_at(object,
bound)` — **never** `object.check`/`object.create`, which are precomputed
against the newest revision (see `Object`'s moduledoc for why that
distinction matters: a healthy DB whose comment sits between two revisions
must verify clean against the *comment-era* shape, and any create for a
missing object must be built from that same shape). Turning a resolved
`%{object:, shape:}` pair into actual SQL is
`PhoenixKit.Migrations.Repair.Executor.create_action/2`'s job, not this
module's — `shape_at/2`'s result is already everything a per-class SQL
builder needs (see that module's moduledoc for why every class *except*
`:column`/`:index`/`:constraint` ends up using the object's own
newest-shape `create` regardless of which revision `shape` selected).

# `resolved`

```elixir
@type resolved() :: %{
  object: PhoenixKit.Migrations.ExpectedSchema.Object.t(),
  shape: PhoenixKit.Migrations.ExpectedSchema.Object.shape()
}
```

One in-scope object, resolved to the shape `bound` selects.

# `class_rank`

```elixir
@spec class_rank(PhoenixKit.Migrations.ExpectedSchema.Object.class()) ::
  non_neg_integer()
```

The additive-safety class order (spec §6.1/§6.2): extensions < functions < sequences < tables < columns < indexes < constraints < seeds.

# `constraint_fk_rank`

```elixir
@spec constraint_fk_rank(
  PhoenixKit.Migrations.ExpectedSchema.Object.class(),
  PhoenixKit.Migrations.ExpectedSchema.Object.shape()
) :: 0 | 1
```

Secondary tier within `class: :constraint`: foreign keys (`shape.type ==
"f"`) sort AFTER every non-FK constraint (PK/UNIQUE/CHECK), regardless of
`since`/`id`.

`class_rank/1` alone guarantees a table's own columns exist before any
constraint on it, but says nothing about constraints ordered against
EACH OTHER — a FK must never be attempted before the PK/UNIQUE it
references. In practice `since(FK) >= since(PK)` always holds for a real
historical chain (Postgres itself refuses to create a FK before the
referenced column has a unique constraint, so the original chain could
never have recorded it the other way round), so `since` already orders
most pairs correctly — the tier below only has to arbitrate the case
`since` cannot: two constraints tied at the same `since` (routine post-
squash, where most pre-floor objects share the floor's `since`), where
the fallback `id` tiebreak is pure alphabetical and has no notion of
"PK before the FK that needs it" (this exact class of bug reproduced
live: `phoenix_kit_file_instances_..._fkey` sorts before
`phoenix_kit_files_pkey` alphabetically, which is backwards — see
`dev_docs/squash/generate_baseline.exs`'s `Differ.constraint_fk_rank/2`,
the sibling fix for the manifest generator's own emission order).

# `execution_order`

```elixir
@spec execution_order([resolved()]) :: [resolved()]
```

Sorts resolved objects for execution: class order first (see
`class_rank/1`), then `since`, then non-FK-before-FK (see
`constraint_fk_rank/2`), then `object.id` — a stable, deterministic
order independent of the manifest's own emission order (which sorts
`{since, class, fk_rank, id}` for diff-friendliness; this sorts for
*execution safety* — class always dominates, since a column must never
be attempted before its table regardless of which version introduced
each).

# `in_scope_invariants`

```elixir
@spec in_scope_invariants([map()], bound :: pos_integer()) :: [map()]
```

Selects the data invariants in scope for `bound` (`invariant.since <= bound`
— the same half of the scope rule as `partition/2`, for
`PhoenixKit.Migrations.ExpectedSchema.DataInvariant.t()` instead of
`Object.t()`).

# `partition`

```elixir
@spec partition(
  [PhoenixKit.Migrations.ExpectedSchema.Object.t()],
  bound :: pos_integer()
) ::
  {in_scope :: [PhoenixKit.Migrations.ExpectedSchema.Object.t()],
   pending :: [PhoenixKit.Migrations.ExpectedSchema.Object.t()]}
```

Partitions `objects` by `object.since <= bound` — the scope rule's first
half. Order within each list is preserved from the input.

# `resolve`

```elixir
@spec resolve(PhoenixKit.Migrations.ExpectedSchema.Object.t(), bound :: pos_integer()) ::
  resolved() | nil
```

Resolves one in-scope object to the shape `bound` selects (the scope
rule's second half — see moduledoc). `nil` for an object whose
`since > bound` — callers are expected to have already filtered via
`partition/2`; this guard exists so a misuse fails as "nothing to
resolve" rather than resolving against a shape that does not exist yet.

---

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