Packages

A minimal kernel for preserving user edits across upstream regeneration cycles

Current section

Files

Jump to
tamale README.md
Raw

README.md

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