# `Linx.Netfilter.Diff`
[🔗](https://github.com/oshlabs/linx/blob/v0.2.0/lib/linx/netfilter/diff.ex#L1)

Structural diff between two `%Linx.Netfilter.Ruleset{}` values,
producing a `%Linx.Netfilter.Patch{}` of the minimum mutations
that turn one into the other.

## Identity rules

  * **Tables, chains, sets, maps**: identity is `name` (within
    family for tables, within table for everything else).
  * **Rules within a chain**: identity is `:tag` when set; for
    untagged rules the diff falls back to positional index.
  * **Set elements**: identity is the element value itself (set
    semantics — no "modified", just add and remove).

## In-place vs delete+recreate

An entity attribute change is rendered as an in-place op when
the kernel supports it, otherwise as a delete+create pair:

  * **Rules**: any structural change → `:replace_rule`
    (`NLM_F_REPLACE` over the kernel-assigned handle from the
    *current* state). Requires the rule to have a `:tag` (or
    stable positional position with no neighbour changes).
  * **Chains**: most attribute changes (type, hook, priority)
    can't be replaced — emit `:delete_chain` + `:create_chain`.
    We conservatively delete+create on any difference.
  * **Tables**: flags / use_count differences → no-op (the
    diff treats tables as opaque containers; their lifecycle is
    managed via the `:owner` flag).
  * **Sets / maps**: declaration changes (key_type, data_type,
    flags) → delete+create. Element changes → element-level
    add/remove ops.

## Untagged rules

When a chain's rule list differs and any of its rules is
untagged, the diff cannot use `:tag`-based identity. It falls
back to: if the lists are identical → no-op; otherwise emit
`:delete_rule` for every current rule and `:create_rule` for
every desired rule (full chain rebuild). The `:reconcile` push
mode rejects this case at its entry point — see
`Linx.Netfilter.Diff.validate_for_reconcile/1`.

# `diff`

```elixir
@spec diff(Linx.Netfilter.Ruleset.t(), Linx.Netfilter.Ruleset.t()) ::
  Linx.Netfilter.Patch.t()
```

Returns the `%Patch{}` that transforms `from` into `to`. Both
inputs are `%Ruleset{}` values — the same shape `pull/1` returns
and `push/2` consumes.

The patch is topologically sorted (deletes before creates of
their dependencies). `Patch.empty?/1` is true iff the rulesets
are structurally equal modulo handle / kernel-assigned values.

# `validate_for_reconcile`

```elixir
@spec validate_for_reconcile(Linx.Netfilter.Ruleset.t()) ::
  :ok | {:error, {:tag_required, {atom(), String.t(), String.t()}}}
```

Validates that `desired` is reconcile-safe: every chain with
more than one rule has tags on all of its rules.

Returns `:ok` or `{:error, {:tag_required, {family, table, chain}}}`.

---

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