# Tamale

[![zread](https://img.shields.io/badge/Ask-Zread-green)](https://zread.ai/SynapticStrings/Tamal)

zongzi isn't 🫔 — this one is.

A minimal kernel for preserving **user edits across upstream regeneration
cycles**. What the user writes is a patch relative to an upstream base;
the base regenerates; after each regeneration every patch must be judged
*still applicable / applicable after transform / dead*. That is a
three-way merge with an explicit merge-base — the same problem `git
patch + base + transform rules` solves — and this kernel is its smallest
honest answer.

Tamale is a greenfield successor designed from a review of
[zongzi](https://github.com/SynapticStrings/Zongzi): it keeps zongzi's two-phase survival doctrine and
compresses its Anchor + Intervention + Timeline subsystems into a single
transport mechanism.

## Quick Start

Tamale separates two questions:

1. *Did the edited thing survive the upstream change?*
2. *Does the edit still apply to the new content?*

Let's Start with an upstream document.

```elixir
source = %{s1: "Hello world."}
{:ok, space} = Space.new([:s1])
```

The user creates a translation for the original text(`s1`):

```elixir
anchor = %Ordinal{
  refs: [:s1],
  at_version: space.version
}

{:ok, patch} =
  Patch.new(
    source.s1,
    %{lang: :zh, text: "你好，世界。"}
  )
```

At this point, the patch means:

> "Apply `你好，世界。` to `s1`, but only if `s1` is still based on `Hello world.`."

Suppose the upstream generator splits `s1`:

```elixir
{:ok, space} =
  Space.apply_op(
    space,
    %Tamale.Op.Split{
      id: :s1,
      children: [:s1, :s1b]
    }
  )

source = %{
  s1: "Hello",
  s1b: " world."
}
```

The important part is that the **identity** of the first child survives the split:

```text
s1
│
├── s1   ← original identity survives
└── s1b
```

Then transport the user's anchor.

```elixir
{:ok, anchor} = Transport.transport(anchor, space)
```

The anchor survived the structural change, so the translation is still attached to `s1`.

Dive to 2nd phase, check whether the patch still applies.

```elixir
case Patch.resolve(patch, source.s1) do
  {:ok, payload} ->
    IO.puts("APPLY: #{payload.text}")

  {:conflict, :base_changed} ->
    IO.puts("CONFLICT: the source changed")

  {:error, reason} ->
    IO.puts("ERROR: #{inspect(reason)}")
end
```

The result is:

```text
CONFLICT: the source changed
```

This is intentional.

The anchor survived the split, but `s1` is no longer the text the user originally edited:

```text
base:     "Hello world."
current:  "Hello"
```

So Tamale reports:

```text
structural survival  → yes
semantic survival    → no
```

That distinction is the core of Tamale's two-phase survival model.

## Architecture

### Layering

Tamale couldn't works without your task, so it needs combination with kernel, policy & adapters/host.

```plain
kernel   : Space(id, order, version) · Op · Anchor/Transport · Patch     ← this package
policy   : relocation choice, clip-vs-conflict, digest chunk granularity ← callbacks
adapters : Tempo→Warp · curve samplers · windowing · score theory · engine bindings
```

The kernel holds no domain data and no engine contract.

### Core Concepts

Tamale has four small building blocks:

* **`Tamale.Space` — where things live**

  A `Space` is the versioned world being edited. It gives stable ids
  to objects and records every change as an `Op` in a linear log.

  ```elixir
  {:ok, space} = Space.new([:a, :b, :c])
  ```

  After an edit, the space gets a new version and the edit is added to
  its log. The log is what lets Tamale move old anchors through later
  changes.

* **`Tamale.Op` — what changed**

  An `Op` describes an edit explicitly:

  ```text
  Insert  Delete  Split  Merge  Move  Retime
  ```

  Tamale works from these edit intents rather than trying to infer
  changes by comparing two states.

  A raw `diff(old, new)` adapter exists for callers that only have
  snapshots, but it is a fallback rather than the kernel's source of
  truth.

* **`Tamale.Anchor` + `Tamale.Transport` — where an edit should go**

  A patch is attached to an `Anchor`, not directly to a particular
  version of the source.

  When the source changes, `Transport` moves that anchor through the
  `Space`'s op log:

  ```elixir
  {:ok, anchor}
  {:clip, covered, lost}
  {:ambiguous, candidates}
  {:undefined, reason}
  ```

  Tamale supports three anchor shapes:

  * `Ordinal` — identifies objects and their structural position.
  * `Metric` — identifies coordinate intervals and moves through a
    `Tamale.Warp`.
  * `Relative` — identifies an interval relative to another object.

  Coordinates use exact rationals (`Tamale.Coord`); floats are rejected.

* **`Tamale.Patch` — whether the edit still applies**

  A patch is a payload together with the digest of the content it was
  created from:

  ```text
  patch = (base_digest, payload)
  ```

  Resolving a patch is deliberately strict:

  ```elixir
  {:ok, payload}
  {:conflict, :base_changed}
  ```

  If the current content has the same digest as the original base, the
  patch applies. Otherwise, it conflicts.

  There is no fuzzy matching or tolerance knob in the kernel.

#### How they fit together

The whole flow is:

```text
          Op
          │
          ▼
       Space ──────► new version
          │
          │ transport
          ▼
       Anchor
          │
          │ locate
          ▼
        Patch
          │
          │ resolve
          ▼
     apply / conflict
```

This gives Tamale two deliberately separate questions:

```text
1. Did the edited location survive the upstream change?
   → Anchor + Transport

2. Does the edit still apply to the new content?
   → Patch + Digest
```

That separation is the core of Tamale's two-phase survival model.

## Invariants

- Edit intent is first-class; heuristics live only in the `diff` fallback.
- Structural survival (transport, at edit time) and semantic survival
  (`Patch.resolve`, at render time) are separate phases.
- No tolerance knobs; conflicts surface explicitly.
- Single writer: one linear log. (Offline/collaboration would
  reintroduce tombstones — as a deliberate extension, not a heuristic.)
- Kernel conventions, not policy: a split's first child inherits the
  parent id; a merge's `into` is `hd(ids)`; ids are never reused.

## Status: scaffold

Working and tested:

- `Space` op application with validation, versioning, log, truncation
- `Transport` for all three anchor shapes:
  - `Ordinal` (delete/split/merge/move/retime, conjunctive refs,
    head-state adjacency, `boundary_merged` when a merge collapses an
    `adjacent?` anchor's refs, truncated/future versions)
  - `Metric` (warp-fold transport; warps come from a Caller provider —
    the kernel holds no spans; partial survival surfaces as first-class
    `{:clip, covered, lost}`; the folded warp is available via
    `Transport.fold_warp/4` for `ChannelAdapter.warp_payload/2`)
  - `Relative` (Ordinal-rule host transport; absolute interval derived
    via `Anchor.project/3`; offsets may be negative and overhang the
    host)
- `Warp` algebra over exact rational coordinates (`Tamale.Coord`):
  `from_segments/1` (monotonicity-validated assembly), `compose/2`,
  `invert/1`, `map_interval/2` — a 1/3 tempo produces thirds, never
  float dust
- `Patch` digest resolve over canonical digests (`Tamale.Digest` —
  floats/structs/tuples rejected; atom keys encoded by name; spec +
  worked examples in `docs/spec/canonical-digest.md`)
- `ChannelAdapter.warp_payload/2` — the single channel-adapter callback
- JSON conformance vectors (`test/conformance/`, format v1): 40 scenarios
  across space/ordinal/metric/relative/digest/resolve, seeded from
  zongzi's `GOLDEN_SCENARIOS.md` including the deliberate semantic flips
  (G-AN-02 merge, G-INT-05 seconds anchor). Coordinates travel as
  integers or `"num/den"` strings; the metric family pins exact rational
  arithmetic (thirds, composed fractional scales). The Elixir
  implementation is now the reference runner; other languages implement
  against the vectors.

Guides and specs:

- `docs/zh/guide/caller-guide-zh.md` — the Caller orchestration contract
  (also the equinox migration manual): trio layout, edit-loop op
  conventions, two-phase survival, warp/digest obligations, engine
  protocol requirements, self-check list
- `docs/spec/canonical-digest.md` — portable digest spec v1

Done (implemented in the downstream `coconut` editor core):

- Warp-provider reference example — `Coconut.Edit.WarpProvider`
   constructs tick/frame warps from tempo maps and span tables, including
   the `T_new ∘ W_tick ∘ T_old⁻¹` composition for frame-addressed,
   score-following anchors.
- `diff(old, new)` fallback adapter — `Coconut.Edit.Diff` infers the
   six canonical ops from raw state pairs for import/reload/collaboration.

Not yet:

- Chunked digest helper — the pattern is settled
   (`docs/decisions/0006`); an optional helper module may follow when
   projection scale makes monolithic digest materialization expensive.

Design decisions: `docs/decisions/`.

## License

MIT (same as zongzi).
