Tamale
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:
- Did the edited thing survive the upstream change?
- 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
δ½ ε₯½οΌδΈηγtos1, but only ifs1is still based onHello 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 liveA
Spaceis the versioned world being edited. It gives stable ids to objects and records every change as anOpin 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 changedAn
Opdescribes an edit explicitly:Insert Delete Split Merge Move RetimeTamale 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 goA patch is attached to an
Anchor, not directly to a particular version of the source.When the source changes,
Transportmoves that anchor through theSpace'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 aTamale.Warp.Relativeβ identifies an interval relative to another object.
Coordinates use exact rationals (
Tamale.Coord); floats are rejected.Tamale.Patchβ whether the edit still appliesA 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
difffallback. - 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
intoishd(ids); ids are never reused.
Status: scaffold
Working and tested:
Spaceop application with validation, versioning, log, truncationTransportfor all three anchor shapes:Ordinal(delete/split/merge/move/retime, conjunctive refs, head-state adjacency,boundary_mergedwhen a merge collapses anadjacent?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 viaTransport.fold_warp/4forChannelAdapter.warp_payload/2)Relative(Ordinal-rule host transport; absolute interval derived viaAnchor.project/3; offsets may be negative and overhang the host)
Warpalgebra 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 dustPatchdigest resolve over canonical digests (Tamale.Digestβ floats/structs/tuples rejected; atom keys encoded by name; spec + worked examples indocs/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'sGOLDEN_SCENARIOS.mdincluding 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 listdocs/spec/canonical-digest.mdβ portable digest spec v1
Done (implemented in the downstream coconut editor core):
- Warp-provider reference example β
Coconut.Edit.WarpProviderconstructs tick/frame warps from tempo maps and span tables, including theT_new β W_tick β T_oldβ»ΒΉcomposition for frame-addressed, score-following anchors. diff(old, new)fallback adapter βCoconut.Edit.Diffinfers 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).