# `MetamorphicLog.Ingest`
[🔗](https://github.com/moss-piglet/metamorphic_log/blob/main/lib/metamorphic_log/ingest.ex#L1)

Deterministic ingestion / scale primitives for an Elixir operator pipeline.

These are the building blocks for *operating* a log (not just verifying one):
content **dedup keys**, tile **flush geometry**, and Merkle **recomputation**
over tile bytes. They are pure and side-effect-free — sequencing state and
tile storage I/O stay on the BEAM side, which is the idiomatic split (NIFs do
CPU-bound math; the BEAM owns state and I/O).

### Sequencing & tile I/O live in Elixir

The Rust core's `Sequencer` and `TileReader` are intentionally *not* wrapped:
a sequencer is a per-namespace monotonic counter best kept as BEAM state
(e.g. an `Agent`/`GenServer` or a DB column), and tile reads are storage I/O.
Instead, your pipeline reads tile bytes from wherever they live and feeds them
to `tile_hashes/4`, `parent_hash/1`, and `recompute_root/1` — the dirty-CPU
hashing primitives — to reproduce a root from tiles.

Binary values are **base64-encoded**; tile paths are returned as
`tile/<level>/<index>[.p/<width>]` strings.

# `dedup_key_from_record`

```elixir
@spec dedup_key_from_record(namespace :: String.t(), payload_b64 :: String.t()) ::
  {:ok, String.t()} | {:error, String.t()}
```

Content dedup key for a `payload` under `namespace`, base64-encoded (64 bytes).

Domain-separated as `SHA3-512_with_context("metamorphic-log/ingest-dedup-content/v1", lp(ns) || lp(payload))`.

## Example

    {:ok, key} = MetamorphicLog.Ingest.dedup_key_from_record("acme", payload_b64)

# `dedup_key_from_token`

```elixir
@spec dedup_key_from_token(namespace :: String.t(), token_b64 :: String.t()) ::
  {:ok, String.t()} | {:error, String.t()}
```

Token dedup key for an idempotency `token` under `namespace`, base64-encoded.

Uses the `metamorphic-log/ingest-dedup-token/v1` context.

# `entry_bundles_to_flush`

```elixir
@spec entry_bundles_to_flush(non_neg_integer(), non_neg_integer()) ::
  {:ok, [String.t()]} | {:error, String.t()}
```

Entry-bundle paths that must be (re)written when growing from `old_size` to
`new_size`. Returns `{:ok, [path]}` or `{:error, reason}`.

# `parent_hash`

```elixir
@spec parent_hash([String.t()]) :: {:ok, String.t()} | {:error, String.t()}
```

Compute the parent hash above a full tile's `256` (or fewer, for a partial
tile) node hashes. Returns `{:ok, hash_b64}` or `{:error, reason}`. Dirty CPU.

# `partial_width`

```elixir
@spec partial_width(0..63, non_neg_integer()) :: 0..256
```

Width (number of leaves, `1..256`) of the partial tile at `level` for a tree
of `size` leaves; `0` if that tile is absent.

# `recompute_root`

```elixir
@spec recompute_root([String.t()]) :: {:ok, String.t()} | {:error, String.t()}
```

Recompute the RFC 6962 Merkle root from an ordered list of base64 **leaf
hashes**. Returns `{:ok, root_b64}` or `{:error, reason}`. Dirty CPU.

# `tile_hashes`

```elixir
@spec tile_hashes(0..63, non_neg_integer(), 1..256, String.t()) ::
  {:ok, [String.t()]} | {:error, String.t()}
```

Parse the node hashes out of a tile's bytes.

`bytes` is the base64-encoded tile blob; `level`/`index`/`width` identify the
tile. Returns `{:ok, [hash_b64]}` (each 32 bytes, base64) or
`{:error, reason}`. Runs on a dirty CPU scheduler.

# `tiles_for_size`

```elixir
@spec tiles_for_size(non_neg_integer()) :: [String.t()]
```

Every tile path needed to represent a tree of `size` leaves.

# `tiles_to_flush`

```elixir
@spec tiles_to_flush(non_neg_integer(), non_neg_integer()) ::
  {:ok, [String.t()]} | {:error, String.t()}
```

Tile paths that must be (re)written when the tree grows from `old_size` to
`new_size` leaves. Returns `{:ok, [path]}` or `{:error, reason}`.

---

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