# Concepts

tether borrows [jj](https://jj-vcs.dev)'s working-copy and operation-log ideas (`new REV` to start on top of a commit, the stale-working-copy check, an op log with `undo`). It applies them to external systems that keep their own history. This page is the model in one sitting; the [Overview](index.md#vocabulary) has the eight terms it uses.


# The working copy is the manifests

A dataset is a directory holding `tether.toml` and one manifest per object, `.tether/objects/<key>.toml`. A manifest names the object ([kind](../reference/Action.md#tether.Action.kind) and `locator`), its policy, and once committed its recorded `state` and [pin](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.pin). Committing a dataset commits those manifests to the enclosing git or jj repository, so history, branching, workspaces and sharing are the VCS's. What the VCS cannot see, what tether did to the *stores*, goes in tether's [operation log](configuration.md#operation-log).


# Commits hold pins; bookmarks hold branches

Two things are versioned, in two places.

A **dataset commit** records, for every object, an exact *state* and, where the system can hold one, a *pin*: a native ref that keeps that state alive. Nothing in a commit points at anything that moves: not a branch, not "latest", not a path's current contents. That is what makes `tether open KEY --rev C` a reproducible read months later, and what [verify](../reference/ObjectStatus.md#tether.ObjectStatus.verify) checks.

A **bookmark** is where writes go. The working copy (`.tether/workspace.toml`, per checkout) records which bookmark it works on and, per object, the branch that stands for it: its *working ref*.

| Working copy is on | Each Forkable object's working ref | Fingerprinted by [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit)? |
|----|----|----|
| the trunk bookmark (`main`) | its upstream branch (`locator.branch`) | yes: the head is what the next commit pins |
| another bookmark, branch created | `tether.ws.<dataset>.<bookmark>` in its system | yes |
| another bookmark, branch not yet created (lazy) | pending: forked from the pin on the first writable [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) | no: until then it sits at the pin |
| no bookmark | none: read-only | no |

Objects whose system has no branches (files, Delta, DuckLake) have no working ref: they keep their pin until a trunk `pull` reads them. The rule is that [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) contacts an object only if this checkout can have moved it, the way a file you did not edit stays as the parent commit had it. `locator.branch` is the object's *upstream*: what the trunk bookmark stands for, what `pull` reads, and what [promote](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.promote) lands on.


# State, pin, fork

For each object a backend implements three things, in increasing order of what the system must support:

- **State**: what the object *is* now, such as an Icechunk [snapshot_id](../reference/handles.IcebergHandle.md#tether.handles.IcebergHandle.snapshot_id), a Neon `(lsn, commit_xid, branch)`, a Delta [version](../reference/handles.DeltaHandle.md#tether.handles.DeltaHandle.version), or a file's sha256. Comparing states is how drift is detected without watching anything.
- **Pin**: a durable native reference to a state (an Icechunk tag, a Neon child branch, a git, Lance or Dolt tag), named `tether.<dataset-id>.<hash>`. The hash is content-addressed from `(kind, locator identity, state)`, so re-committing an unchanged object is a no-op. The dataset id keeps `gc` inside this dataset's [namespace](configuration.md#dataset-id).
- **Fork**: a writable branch created off a pin, or for `pin = "record"` objects off the recorded state. `tether new -b NAME` decides one per Forkable object, `tether.ws.<dataset-id>.<bookmark>`, created by the first writable [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) (or at once with `--eager`).


# Capability tiers

Backends declare capabilities, and commands degrade explicitly by tier:

- **Observed** ([FINGERPRINT](../reference/testing.Capability.md#tether.testing.Capability.FINGERPRINT)): drift detection only; [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) records the state as `recoverable = false`. A local directory, an unversioned prefix.
- **Addressable** (`+ ADDRESSABLE`): the recorded state can be read back later with nothing to create or collect. An object in a versioned bucket, a Delta version, a DuckLake snapshot.
- **Pinnable** (`+ PIN`): a durable native ref can be created and deleted; [verify](../reference/ObjectStatus.md#tether.ObjectStatus.verify) and `gc` apply.
- **Forkable** (`+ FORK`): a writable branch can be made off a pin, so [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) forks and [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) returns a writable handle. Icechunk, Neon, Iceberg, Lance, Dolt, git.

Orthogonal flags such as [PROMOTE](../reference/testing.Capability.md#tether.testing.Capability.PROMOTE), [MERGE](../reference/testing.Capability.md#tether.testing.Capability.MERGE), [CONDITIONAL_REF](../reference/testing.Capability.md#tether.testing.Capability.CONDITIONAL_REF) and [DIFF](../reference/testing.Capability.md#tether.testing.Capability.DIFF) are listed per backend in [Backends](backends.md), and defined in [Writing a Backend](extending.md#capabilities-decide-behavior).


# Handles, not data

tether is never in the data path. [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) returns a typed handle carrying the native address (an Icechunk session on the right branch or tag, a Postgres URL, a `DeltaTable` at a version) and steps aside. You read and write with the system's own library.


# Store writes are plans

Most commands that write to a store compute a read-only *plan*, then *apply* it; apply refuses ([StalePlanError](../reference/StalePlanError.md#tether.StalePlanError)) if the world no longer matches what the plan saw ([Plans](reclaiming-storage.md#preview-before-you-write-plans)).


# jj or tether?

Both work on the same repository, so the question comes up constantly. The rule: **an operation gets a tether command when it has two halves, one in the VCS and one in the stores, that must happen together.** If it only touches files and history, use jj or git directly. tether notices what it needs to: a moved working copy makes objects stale, and a vanished dataset commit shows in [status](../reference/backends.VerifyReport.md#tether.backends.VerifyReport.status) and `ops`.

The nouns map one to one. A jj *bookmark* stands for one branch per system. jj's *snapshot on every command* is tether's **snapshot** on demand: a concurrent fingerprint on [verify](../reference/ObjectStatus.md#tether.ObjectStatus.verify), `status --snapshot` and `snapshot`, and of the bookmark's branches on [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit). A *stale working copy* is a bookmark whose committed state changed after its branches were forked. A *detached head* is an object registered `--at` a state, until a trunk `pull` moves it onto the branch. A *revision with no bookmark* is a read-only working copy.

| You want to | Use | Because |
|----|----|----|
| record the dataset's state | `tether commit` (jj: `jj commit`) | pins the bookmark's branches, writes their states into the manifests, commits, and moves the bookmark |
| start a line of work | `tether new -b NAME [REV]` (jj: `jj new -b`) | creates the bookmark *and* a branch per system named after it |
| switch to a bookmark | `tether new NAME` (jj: `jj new <bookmark>`) | moves the working copy *and* adopts that bookmark's branches |
| look at another revision without writing | `jj new REV` / `git switch` | nothing to fork; [status](../reference/backends.VerifyReport.md#tether.backends.VerifyReport.status) says the working copy left the bookmark |
| compare two positions | `tether diff [A] [B]` (jj: `jj diff`) | an object-level manifest diff; `--content` adds native diffs |
| browse an object's own history | `tether log KEY` (jj: `jj log`) | the system's snapshots, versions or commits; the ids feed `add --at` |
| take what upstream has now | `tether pull` on `main` (jj: `jj git fetch`) | reads every upstream branch and branch-less object, and commits what moved |
| land a bookmark on `main` | `tether promote` (jj: `jj bookmark set main -r feature`) | moves each system's upstream branch, then the trunk bookmark |
| throw a bookmark away | `tether drop NAME` | drops its commits and the bookmark, then releases its pins and branches |
| reset one object to an older state | `tether restore KEY --from REV` (jj: `jj restore --from`) | the branch lives in a store; everything else stays |
| drop dataset commits | `tether abandon REV` (jj: `jj abandon`) | descendants keep their manifests, and the freed pins are planned for `gc` |
| describe, squash, rebase, split, merge | jj / git | manifests are files; run `tether gc` if commits disappeared, and `tether verify` and `tether new` after a merge |
| rename or delete a bookmark | `jj bookmark rename/delete` | [status](../reference/backends.VerifyReport.md#tether.backends.VerifyReport.status) then says so; `gc --prune-bookmarks` releases the old branches |
| push, fetch, review | jj / git | [Sharing and CI](sharing-and-ci.md) |
| open or finish a second checkout | `jj workspace add`; `tether forget-workspace` | each checkout is its own tether workspace; forgetting one leaves the bookmark's branches |
| release what history no longer names | `tether verify`, then `tether gc` | pins and branches live in the stores |
| undo tether's last operation | `tether undo` (jj: `jj undo`) | the VCS cannot see what tether did to the stores ([Undo](branching-and-writing.md#undo)) |
| rebuild a pin or branch a manifest names | `tether repair` | manifests are the source of truth |
| publish history as SQL | `tether export` / `publish` / `import` (experimental) | a derived view of the manifests |

Two habits keep the halves aligned: `tether status` after any VCS operation that moved the working copy or changed manifests, and `tether new` before writing again.


## Where tether is not jj

- **Writes need a bookmark.** A store branch must exist before a write can land, so the bookmark is the handle both sides share. Reads (`open --rev`, [status](../reference/backends.VerifyReport.md#tether.backends.VerifyReport.status), [verify](../reference/ObjectStatus.md#tether.ObjectStatus.verify)) work on anonymous changes, as in jj.
- **[commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) moves the bookmark.** jj bookmarks do not follow commits; a tether bookmark must, or its branches' heads would stop matching the commit that names them. It happens in one jj operation, so `jj undo` takes both back. [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) also starts nothing new.
- **Snapshots are on demand.** Reading a state is a network round trip, so [status](../reference/backends.VerifyReport.md#tether.backends.VerifyReport.status) is local, and `status --snapshot`, [verify](../reference/ObjectStatus.md#tether.ObjectStatus.verify) and [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) fan out.
- **The stores are remotes.** Pins are what commits reference, bookmark branches are local branches, `pull` is fetch, and [promote](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.promote) is the push to `main`.


# What tether does not do

- Sit in the data path, or run between commands: there is no daemon, and every state is compared when a command asks.
- Cross-system atomicity: a commit records each state within a capture window, and point-consistency requires quiesced writers.
- Reimplement history, branching or sharing, or query across systems. `ops`, `undo` and `repair` cover only what tether did to the stores.
