zread

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: 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.

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

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

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:

{: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:

s1
β”‚
β”œβ”€β”€ s1   ← original identity survives
└── s1b

Then transport the user's anchor.

{: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.

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:

CONFLICT: the source changed

This is intentional.

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

base:     "Hello world."
current:  "Hello"

So Tamale reports:

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.

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.

    {: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:

    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:

    {: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:

    patch = (base_digest, payload)

    Resolving a patch is deliberately strict:

    {: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:

          Op
          β”‚
          β–Ό
       Space ──────► new version
          β”‚
          β”‚ transport
          β–Ό
       Anchor
          β”‚
          β”‚ locate
          β–Ό
        Patch
          β”‚
          β”‚ resolve
          β–Ό
     apply / conflict

This gives Tamale two deliberately separate questions:

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).