# `Minga.RenderModel.Window.ContentDigest`
[🔗](https://github.com/jsmestad/minga/blob/main/lib/minga/render_model/window/content_digest.ex#L1)

Incrementally maintained content digest for a window's resident row set (#2658).

The GUI adapter gates each window's content frame-emit on whether the window's
rows changed since the last emit. With full-document residence a window's row
list is the whole document, so re-hashing the entire list every frame is
O(document) and dominates edit-frame build cost. This digest replaces that
whole-list hash with a value that updates in O(1) per changed row and stays
correct across row insertion and deletion.

## Keying

The digest is a fold over per-row *cells*, where a cell is
`phash2({row_id, content_hash})`. `row_id` already encodes the row's buffer
line (`Row.stable_id/4`), so a cell captures both a row's identity/position and
its rendered content. Two row lists produce the same digest with overwhelming
probability when they carry the same multiset of `{row_id, content_hash}` pairs
(see Collision behaviour below for the probabilistic bound).

## Algebra

Cells are combined with `bxor`, which is associative, commutative, and
self-inverse. That gives O(1) `add`/`remove`/`update` and makes the digest
independent of iteration order (position is already carried inside `row_id`, so
order independence never hides a real change). Editing a row back to its prior
content restores the prior cell and therefore the prior digest, so no spurious
frame-emit occurs.

## Collision behaviour

The digest is `phash2`-derived, so a single-row content change is missed only
when the old and new cells collide (~1 in 2^27 per changed row). For multi-row
splices the deltas of several rows can also XOR-cancel even when no single row
collides; the overall miss probability stays in the same ~2^-27 order. This
matches the collision profile of the whole-list `phash2` it replaces; it does
not add risk. Row ids are assumed unique within a window (guaranteed by
`Row.stable_id/4` per position); the digest is a set digest under that
assumption.

# `t`

```elixir
@opaque t()
```

An opaque non-negative integer digest.

# `add`

```elixir
@spec add(t(), Minga.RenderModel.Window.Row.row_id(), non_neg_integer()) :: t()
```

Folds a row into the digest.

# `cell`

```elixir
@spec cell(Minga.RenderModel.Window.Row.row_id(), non_neg_integer()) ::
  non_neg_integer()
```

Returns the per-row cell for a `{row_id, content_hash}` pair.

# `empty`

```elixir
@spec empty() :: t()
```

Returns the digest of an empty row set.

# `of_pairs`

```elixir
@spec of_pairs([{Minga.RenderModel.Window.Row.row_id(), non_neg_integer()}]) :: t()
```

Computes the digest of a list of `{row_id, content_hash}` pairs from scratch.

# `of_rows`

```elixir
@spec of_rows([Minga.RenderModel.Window.Row.t()]) :: t()
```

Computes the digest of a row list from scratch.

This is the O(document) reference used on a full rebuild and as the oracle in
tests; the incremental `add`/`remove`/`update` operations must always agree
with it.

# `remove`

```elixir
@spec remove(t(), Minga.RenderModel.Window.Row.row_id(), non_neg_integer()) :: t()
```

Removes a row from the digest.

Self-inverse with `add/3`: removing a row that was previously added restores
the digest to its pre-add value.

# `update`

```elixir
@spec update(
  t(),
  Minga.RenderModel.Window.Row.row_id(),
  non_neg_integer(),
  non_neg_integer()
) :: t()
```

Replaces a row's content hash under the same id.

---

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