# `Mutare.Ecto.ValidationBoundary`
[🔗](https://github.com/foxbenjaminfox/mutare_ecto/blob/v0.4.2/lib/mutare/ecto/validation_boundary.ex#L1)

Swap a `validate_number/3` bound between its strict and non-strict form — the changeset
counterpart of the in-query `:comparison` swap. `validate_number(:age, greater_than: 0)` →
`validate_number(:age, greater_than_or_equal_to: 0)`. "Is the bound *itself* tested?" — the two
validations accept the same values except one exactly on the bound, so the mutant survives
unless a test builds a changeset whose value equals it and asserts the validation result. Family
`:validation_boundary`; equivalence-sensitive (`Mutare.Ecto.Equivalence`), because a kill needs
exactly that fixture.

Four swaps, in two pairs: `greater_than` ↔ `greater_than_or_equal_to` and `less_than` ↔
`less_than_or_equal_to`. `equal_to`/`not_equal_to` are deliberately not swapped: the mutant
rejects the one value the written validation accepts (or the reverse), which any happy-path test
kills — no signal beyond the whole-call `:validation_drop`. `validate_length`'s `min`/`max`/`is`
are inclusive with no strict counterpart, so core's integer family provides their off-by-one
mutations (the value stays an ordinary expression — `Mutare.Ecto.Changeset.Routing`).

One mutant per swappable option; every other option (`message:`, a second bound) is kept as
written. Matched like the stage drops (`Mutare.Ecto.Changeset`): the call resolves to
`Ecto.Changeset` in any spelling, and the options are the trailing argument in the direct and
piped forms alike. A non-keyword options argument (`validate_number(cs, :age, opts)`) yields
nothing — there is no written key to swap. Delivered in place (the call rebuilt in its written
form), reported at and tagged with the **written** key so
`# mutare:ignore[ecto:greater_than]` on a bound's line suppresses its swap.

# `label`

```elixir
@spec label(atom()) :: String.t()
```

The finer `# mutare:ignore` label for a bound swap: the **written** key
(`# mutare:ignore[ecto:greater_than]` leaves a `greater_than:` bound's swap alone). Total over
any key; `swap/1` returns the replacement key or `nil` when no swap exists.

# `mutations`

```elixir
@spec mutations(Macro.t(), Mutare.Ecto.Context.t()) :: [Mutare.Ecto.Tag.t()]
```

Bound-swap mutations for a `validate_number/3` call as `:validation_boundary` tags, or `[]` for
any other call, for a call without a written keyword options list, or for options without a
swappable key.

# `swap`

```elixir
@spec swap(atom()) :: atom() | nil
```

The strict/non-strict counterpart of a `validate_number` option key, or `nil` for any other key.

---

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