# `Ramal.Branch`
[🔗](https://github.com/agustinrojas1/ramal/blob/v0.2.0/lib/ramal/branch.ex#L1)

A branch: the struct a branch process holds and storage persists, plus the pure functions over it.

* `id` - a UUID string
* `name` - free text for people
* `domain` - the Ash domain whose resources the branch covers
* `status` - `:open`, `:proposed`, `:merged`, `:rejected` or `:discarded`
* `base` - what the branch knows about the base rows it touched, by `{resource, key}`: `%{fingerprint: hash | nil,
  snapshot: record | nil}`. The fingerprint is taken when a row is first read or written, the snapshot when it is first
  written, so `diff/2` never queries the database
* `overlay` - see `Ramal.Overlay`
* `log` - the writes the branch captured, newest first so that recording one is O(1); readers use `Enum.reverse/1`
* `inserted_at` - when the branch was created
* `metadata` - a map the host uses freely; Ramal only adds `forked_from` to a fork
* `proposal` - `nil` until `Ramal.propose/2`; then `t:proposal/0`

Nothing here touches a process or storage, so it is tested with plain data.

`inspect/1` prints `id`, `name`, `domain`, `status` and `inserted_at` only. The log keeps the raw input and the actor of
every captured write, which can hold passwords and tokens, and a crash report prints the state of a branch process.

## Example

    iex> branch = Ramal.Branch.new(MyApp.Farm, name: "move herd R7", metadata: %{ticket: 42})
    iex> {branch.name, branch.status, branch.overlay, branch.metadata}
    {"move herd R7", :open, %{}, %{ticket: 42}}
    iex> Ramal.Branch.open?(branch)
    true

# `base_row`

```elixir
@type base_row() :: %{fingerprint: non_neg_integer() | nil, snapshot: map() | nil}
```

What the branch knows about one base row.

# `kind`

```elixir
@type kind() :: :created | :updated | :deleted
```

# `log_entry`

```elixir
@type log_entry() ::
  {Ash.Resource.Actions.action_type(), module(), atom(), map(), term(),
   keyword(), DateTime.t()}
```

A captured write: `{action_type, resource, action_name, input, actor, opts, at}`. `input` is the params the action
was called with, `opts` carries `:key` (the primary key written), `:tenant` and `:nested?` (the write was made by
a hook of another captured action, so replaying that action reproduces it).

# `proposal`

```elixir
@type proposal() :: %{
  proposer: term(),
  reason: String.t() | nil,
  proposed_at: DateTime.t(),
  decision:
    nil
    | %{
        status: :approved | :rejected,
        by: term(),
        reason: String.t() | nil,
        at: DateTime.t(),
        replayed: non_neg_integer() | nil,
        stale: [Ramal.Merge.Result.stale_row()]
      }
}
```

A branch's proposal: who proposed it and why, and once decided, how. `proposer` and `by` are whatever the host passed
(`:proposed_by`, `:decided_by`); `stale` and `replayed` are what the approving merge saw.

# `ref`

```elixir
@type ref() :: Ramal.Overlay.ref()
```

# `status`

```elixir
@type status() :: :open | :proposed | :merged | :rejected | :discarded
```

# `t`

```elixir
@type t() :: %Ramal.Branch{
  base: map(),
  domain: module(),
  id: String.t(),
  inserted_at: DateTime.t(),
  log: list(),
  metadata: map(),
  name: String.t() | nil,
  overlay: Ramal.Overlay.t(),
  proposal: proposal() | nil,
  status: status()
}
```

# `undo`

```elixir
@type undo() :: %{
  ref: ref(),
  overlay: Ramal.Overlay.entry() | :none,
  base: base_row() | :none,
  entry: log_entry()
}
```

What `put_write/6` replaced, so `undo/2` can put it back.

# `decide`

```elixir
@spec decide(t(), :approved | :rejected, DateTime.t(), keyword()) :: t()
```

The branch with the decision on its proposal recorded. `status` is `:approved` or `:rejected`; only a rejection
changes the branch's own status (to `:rejected`), because an approval is followed by the merge that marks it `:merged`.
`details` may carry `:by`, `:reason`, `:replayed` and `:stale`.

## Example

    iex> branch = Ramal.Branch.new(MyApp.Farm)
    iex> branch = Ramal.Branch.propose(branch, nil, nil, ~U[2026-01-01 00:00:00Z])
    iex> rejected = Ramal.Branch.decide(branch, :rejected, ~U[2026-01-02 00:00:00Z], by: %{id: 1}, reason: "not now")
    iex> {rejected.status, rejected.proposal.decision.status, rejected.proposal.decision.reason}
    {:rejected, :rejected, "not now"}

# `diff`

```elixir
@spec diff(t(), keyword()) :: {:ok, [Ramal.Change.t()]} | {:error, Ramal.Error.t()}
```

The branch's changes as `Ramal.Change` structs, in the order the log first touched each row.

Never reads the database: updated and deleted rows are compared with the snapshot in `base`. See
`Ramal.Overlay.diff/3` for the rules; the options are the same.

## Example

    iex> branch = Ramal.Branch.new(MyApp.Farm)
    iex> entry = fn key -> {:create, MyApp.Herd, :create, %{}, nil, [key: key], ~U[2026-01-01 00:00:00Z]} end
    iex> {branch, _} = Ramal.Branch.put_write(branch, :created, {MyApp.Herd, 2}, %{id: 2}, nil, entry.(2))
    iex> {branch, _} = Ramal.Branch.put_write(branch, :created, {MyApp.Herd, 1}, %{id: 1}, nil, entry.(1))
    iex> {:ok, changes} = Ramal.Branch.diff(branch)
    iex> Enum.map(changes, & &1.key)
    [2, 1]

# `discard`

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

The branch after `discard`: status `:discarded` and no overlay, log or base, so a discarded branch costs nothing to keep.

## Example

    iex> branch = Ramal.Branch.new(MyApp.Farm)
    iex> branch = %{branch | overlay: %{{MyApp.Herd, 1} => :deleted}}
    iex> discarded = Ramal.Branch.discard(branch)
    iex> {discarded.status, discarded.overlay}
    {:discarded, %{}}

# `fork`

```elixir
@spec fork(t(), keyword()) :: t()
```

A new open branch that starts as a copy of `parent`: same domain, overlay, log and base.

Overlays, logs and the base are immutable, so the copy shares structure with the parent and neither sees the
other's later changes. `metadata` gets `forked_from: parent.id`.

## Options

  * `:name` - default: the parent's name
  * `:metadata` - default: the parent's metadata

## Example

    iex> parent = Ramal.Branch.new(MyApp.Farm, name: "what if")
    iex> parent = %{parent | overlay: %{{MyApp.Herd, 1} => :deleted}}
    iex> fork = Ramal.Branch.fork(parent, name: "variant")
    iex> {fork.name, fork.overlay == parent.overlay, fork.metadata.forked_from == parent.id, fork.id != parent.id}
    {"variant", true, true, true}

# `new`

```elixir
@spec new(module(), keyword()) :: t()
```

A new open branch for `domain` with a fresh id.

## Options

  * `:name` - a label for people, default `nil`
  * `:metadata` - a map for the host, default `%{}`

## Example

    iex> branch = Ramal.Branch.new(MyApp.Farm)
    iex> {branch.domain, branch.name, branch.log}
    {MyApp.Farm, nil, []}

# `open?`

```elixir
@spec open?(t()) :: boolean()
```

Whether the branch still accepts reads, writes and merges.

## Example

    iex> Ramal.Branch.open?(%Ramal.Branch{id: "b", domain: MyApp.Farm, inserted_at: DateTime.utc_now(), status: :merged})
    false

# `propose`

```elixir
@spec propose(t(), term(), String.t() | nil, DateTime.t()) :: t()
```

The branch after `Ramal.propose/2`: status `:proposed`, frozen until someone decides, with the proposal recorded.

## Example

    iex> branch = Ramal.Branch.new(MyApp.Farm)
    iex> proposed = Ramal.Branch.propose(branch, %{id: 7}, "move R7 before the rain", ~U[2026-01-01 00:00:00Z])
    iex> {proposed.status, proposed.proposal.proposer, proposed.proposal.reason, proposed.proposal.decision}
    {:proposed, %{id: 7}, "move R7 before the rain", nil}

# `put_fingerprints`

```elixir
@spec put_fingerprints(t(), %{optional(ref()) =&gt; non_neg_integer()}) :: t()
```

The branch after reading base rows with these fingerprints.

A row the branch already knows keeps its first fingerprint: staleness is measured from the first touch.

## Example

    iex> branch = Ramal.Branch.new(MyApp.Farm)
    iex> branch = Ramal.Branch.put_fingerprints(branch, %{{MyApp.Herd, 1} => 7})
    iex> branch = Ramal.Branch.put_fingerprints(branch, %{{MyApp.Herd, 1} => 8})
    iex> branch.base
    %{{MyApp.Herd, 1} => %{fingerprint: 7, snapshot: nil}}

# `put_write`

```elixir
@spec put_write(t(), kind(), ref(), map(), base_row() | nil, log_entry()) ::
  {t(), undo()}
```

The branch after one captured write.

`kind` says what the write did to the row `ref`, `record` is the row as the action left it, and `base_row` is the
row as the action found it (`nil` for a created row). The first time the branch writes an existing row, its snapshot
goes into `base`, keeping a fingerprint taken by an earlier read if there is one. `entry` is prepended to the log.

Returns the new branch and an `undo` that `undo/2` uses to take the write back when the action that made it fails.

## Example

    iex> branch = Ramal.Branch.new(MyApp.Farm)
    iex> entry = {:update, MyApp.Herd, :update, %{name: "b"}, nil, [key: 1], ~U[2026-01-01 00:00:00Z]}
    iex> before = %{fingerprint: 7, snapshot: %{id: 1, name: "a"}}
    iex> {branch, _undo} = Ramal.Branch.put_write(branch, :updated, {MyApp.Herd, 1}, %{id: 1, name: "b"}, before, entry)
    iex> {branch.overlay, branch.base, length(branch.log)}
    {%{{MyApp.Herd, 1} => {:updated, %{id: 1, name: "b"}}}, %{{MyApp.Herd, 1} => %{fingerprint: 7, snapshot: %{id: 1, name: "a"}}}, 1}

# `undo`

```elixir
@spec undo(t(), [undo()]) :: t()
```

The branch with the writes described by `undos` taken back, newest write first.

Only the refs and log entries the undos name are touched, so a write another process made in the meantime stays.

## Example

    iex> branch = Ramal.Branch.new(MyApp.Farm)
    iex> entry = {:destroy, MyApp.Herd, :destroy, %{}, nil, [key: 1], ~U[2026-01-01 00:00:00Z]}
    iex> {written, undo} = Ramal.Branch.put_write(branch, :deleted, {MyApp.Herd, 1}, %{id: 1}, %{id: 1}, entry)
    iex> Ramal.Branch.undo(written, [undo]) == branch
    true

---

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