- Feature Name: `horizons` (serving clones that hold history only from a
  check-in forward: the content horizon through the sync front unchanged,
  the structural horizon from quarry's own indexes; the history below the
  horizon answered by the API; which consumer gets which tool)
- Start Date: 2026-10-17
- Status: Draft — waits on the corpus measurement in `draft-fossil-horizon-00`
  Appendix A step 4; nothing here is built
- Depends on: RFC 0001 §2 (store access through the zit binary), §7 (sync
  fronting), §9 (the ops layer generates the API), §11 (disposability);
  RFC 0009 (archives, premerge, runners); RFC 0021 (agent accounts, run-scoped
  tokens); zit's `draft-fossil-horizon-00` (the wire, the seed, the held set)
  and `draft-fossil-sparse-cone-00` (the cone the horizon is a member of)

## Summary
[summary]: #summary

A horizon clone holds a repository from a check-in C forward. zit's horizon
draft defines two cuts. The **content horizon** keeps every structural
artifact and elides the file blobs no held check-in references; it needs
nothing from quarry beyond fronting `/xfer`, because the zit subprocess
serves it. The **structural horizon** also omits the manifests below C and
stands a **seed**, a private control artifact asserting the tag state at C,
in their place; it needs an origin that mints seeds, labels the hashes that
cross the horizon, and answers merge-base. quarry is that origin, and this
RFC says how: the seed and the labels are derived from the index plane the
same way every page is, the merge-base answer comes from the same ancestry
index, and all three pass the §11 disposability test because they are pure
functions of store state.

The history a horizon clone does not hold is still on quarry. The RFC names
the API that answers for it (annotate, path history, diff and file at a
check-in, already owed under principle 5), adds one read endpoint quarry
lacks (an artifact by hash with immutable caching), and settles which
consumer gets which tool: build-and-test runners already fetch archives and
keep doing so; persistent workers keep a warm clone; cold agent
environments get the horizon clone, content first.

## Motivation
[motivation]: #motivation

RFC 0001 made quarry the always-on side of zit. The store is the truth and
quarry derives everything else, so a clone that leaves history on quarry
loses nothing a person cannot get back by asking quarry. That is what makes
a shallow clone safe to offer here when it is not safe to offer against an
arbitrary Fossil peer: there is a dense, indexed, always-reachable copy, and
every page quarry renders is already an answer about history the client
need not hold.

The consumer that forces the question is RFC 0021's agent: a short-lived
environment on a dagnats worker that must clone, commit and push, with no
warm cache and a network between it and the store. RFC 0009's runners do
not have this problem, because premerge removed the clone from the test
path: a runner fetches an archive of a tree and never holds a repository.
Agents cannot use that trick. They write.

zit's horizon draft keeps every artifact's real name and the existing
derivation. What it asks of an origin is small and index-shaped, and
quarry already has the indexes.

## Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

An agent's environment starts with:

```
$ zit clone --horizon https://quarry.example/r/proj
```

quarry's sync front sees the horizon pragma, computes the held set from
its ancestry index, and the zit subprocess sends every structural artifact
and the blobs of the held check-ins. The clone opens, derives branch state
exactly, commits, pushes. The push is an ordinary push.

With `--horizon=structural`, quarry additionally sends the seed for C and
labels every parent hash that lies below C. The clone's timeline ends at a
horizon edge rather than a missing parent. When the agent runs `zit annotate`
on a path older than C, zit prints the horizon and the quarry URL that
answers for it; `zit merge` of a branch whose base is below C asks quarry
for the base and deepens to it before merging.

On the repository's page nothing changes. A horizon clone's check-ins are
the same check-ins, under the same hashes.

## Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

### 1. The content horizon costs quarry nothing new

RFC 0001 §7's front passes pull traffic to the per-repo `zit serve` child
untouched, and the child computes the held set and the keep set from the
store. quarry's only obligations are two it already meets: front the
request, and keep the child's store dense. The capability pragma
`horizon-served content` is the child's, not quarry's.

One front-side rule is new. The §7 gate decodes the request container to
inspect login and push cards. It MUST pass `pragma horizon*` cards through
untouched, including to a child whose version predates them, which ignores
them by the sync protocol's rule.

### 2. The structural horizon: three answers from the index plane

The structural horizon needs an origin to mint seeds, label boundary
hashes, and answer merge-base. Each is a function of the store's state and
nothing else, so each is derived data under RFC 0001 §1 and must pass §11:
delete the data directory, re-ingest, get the same answers.

**Where they are computed.** In quarry, from the per-repo SQLite index that
ingest (§4) already maintains for branch state, leaves and ancestry, not in
the zit child. Two reasons. The child is one-shot and stateless, and an
ancestry walk per request against a large store is the work §4's index
exists to avoid. And the seed asserts the tag state quarry displays; minting
it from the same derivation keeps the clone's view and the page in
agreement by construction.

**How they reach the wire.** The front already decodes the request. For a
request carrying `horizon-structural` it computes the three answers,
encodes them as the reply pragmas the zit draft specifies (`horizon-below`,
`horizon-seed` with a private `file` card, `horizon-mergebase`), and splices
them into the child's reply before re-encoding. The child continues to
decide which manifests and blobs to send; quarry adds only what the child
cannot know. This is the one place the front writes into a reply body, and
it is confined to pragma cards and one private file card.

**The seed.** A control artifact whose T cards apply to C every tag in
force at C: the branch tag with its value, every other propagating tag,
`closed` if set, and C's own non-propagating tags, each with the value the
index holds. D card at minting time, no earlier than the latest tag event
summarized and strictly later than any seed it replaces. U card is the
repository's forge agent login (RFC 0021, forge agents), so a seed is never
attributed to a person. Clear-signed with the forge agent's key when the
repository has one configured.

A seed is minted on request, compared by hash to the one the client offers,
sent only when it differs, and never stored. There is no seed table. A
repository with a thousand horizon clones at a thousand different C's costs
quarry a thousand index reads per sync round and nothing between rounds.

**Boundary labels.** For each manifest the child's reply carries, every P
card naming a check-in not in the held set is a boundary hash. The front
emits `horizon-below` for each, in the reply that carries the manifest. The
held set is the zit draft's `leaves` rule by default.

**Merge base.** `horizon-mergebase A B` is answered from the ancestry index
with the same bounded walk the timeline graph uses. No common ancestor
answers `-`.

**Refusing a seed.** A pushed artifact that is a control artifact applying
to a check-in exactly the tag set the index holds for it, with a U card
naming the forge agent, is a seed that leaked. The front refuses the push
with the sync protocol's error card and the text `seed is private to the
clone that holds it`.

### 3. The push-time cancel check

A commit from a structural horizon that creates a branch writes a cancel
card for the parent branch's symbolic tag, derived from the seed. On every
push the front already inspects manifests (RFC 0007's branch ownership). It
additionally checks each `-sym-*` cancel card on a branch-creating manifest
against the index's branch for the parent. On disagreement it refuses with
`stale seed: parent is on <branch>, not <name>; re-sync to re-seed`. This is
the one route by which a clone's derived state enters an artifact, and
checking it here keeps a wrong seed from ever reaching the store.

### 4. The history below the horizon: the API

The zit draft's `horizon-served` pragma carries a URL. quarry's is
`/api/v1/r/<repo>/history`, a namespace over query ops that exist or are
owed under principle 5:

| Op | Route | Status |
|---|---|---|
| annotate | `GET /api/v1/r/{repo}/annotate/{checkin}/{path}` | exists as a page op; API route to confirm |
| path history | `GET /api/v1/r/{repo}/finfo/{path}?from={checkin}` | owed |
| diff | `GET /api/v1/r/{repo}/diff/{a}/{b}` | exists |
| file at check-in | `GET /api/v1/r/{repo}/file/{checkin}/{path}` | exists |
| merge base | `GET /api/v1/r/{repo}/mergebase/{a}/{b}` | new; the same answer as the pragma |
| artifact by hash | `GET /api/v1/r/{repo}/artifact/{hash}` | new |

All are registered ops in the §9 registry, so they appear in the generated
OpenAPI and pass the §11 snapshot. None is a new capability; the horizon
is the reason to finish the list.

**Artifact by hash** is the one genuinely new read. It returns the artifact's
bytes, `Content-Type: application/octet-stream`, with `Cache-Control:
public, max-age=31536000, immutable`, because an artifact's content is its
name. It is what a horizon clone's targeted demand fetch and an edge cache
both want: one blob, no sync round. Authorization is the repository's read
capability as for every read op. A private artifact (the sync protocol's
private marker) is never served here.

### 5. Which consumer gets which tool

| Consumer | Tool | Why |
|---|---|---|
| Build-and-test runner (RFC 0009) | `/archive/<checkin>` and `/archive/pair:` | Already the design: one tree, no clone, no credential. This RFC changes nothing for it. |
| Persistent worker | a dense clone kept warm per host, synced forward, each job on a copy-on-write copy of the `.zit` file | Pays the clone once per host. Outside quarry's control; written here so the horizon is not sold to it. |
| Cold agent environment (RFC 0021) | horizon clone, content first | Must commit and push; has no warm copy; talks only to quarry. |
| A person at a keyboard | dense | RFC 0001 principle 7 names `zit clone` as the exit, and a default clone that is not a full copy weakens that promise. |

The agent-account token minted for an environment (RFC 0021, run-scoped
tokens) carries no horizon. The clone command does. Making the horizon a
property of the token was considered and rejected: a clone shape is the
client's choice, and encoding it in a credential would make the credential
mean two things.

### 6. Disposability (§11), applied

Three new derived answers, one test each in the §11 corpus run: for a
fixture store and a chosen C, snapshot the seed's T cards, the boundary set,
and a merge-base answer; delete the data directory; re-ingest; the three
must be byte-identical except the seed's D card, which is minting time. The
D card is excluded from the comparison and asserted monotonic instead.

### 7. Build order

Gated on the measurement. Nothing before step 4 of the zit draft's
Appendix A is quarry work.

| Rung | What | Waits on |
|---|---|---|
| 1 | Front passes `horizon*` pragmas through untouched | zit serving the content horizon |
| 2 | `/artifact/{hash}` with immutable caching; `/mergebase/{a}/{b}`; the `finfo` route | nothing |
| 3 | `horizon-served` URL pointed at the history namespace | rung 2 |
| 4 | Seed minting, boundary labels, merge-base pragma splice in the front | the measurement saying structural pays; zit's structural horizon |
| 5 | Push-time cancel check; seed refusal | rung 4 |
| 6 | §11 rows for the three answers | rung 4 |

## Drawbacks
[drawbacks]: #drawbacks

- The front writes into a reply body for the first time. It is confined to
  pragma cards and one private file card, and only on requests that asked
  for it, but RFC 0001 §7's "pull traffic passes through untouched" gains an
  exception and the gate gains a code path that must parse the child's
  reply, not just the request.
- A wrong seed is quarry's wrong derivation delivered to a clone. The
  blast radius is the clone's local view plus the one cancel card, which
  rung 5 catches. It is still the first time quarry's derived state is
  asked to stand in for artifacts rather than describe them.
- The structural horizon is quarry-only. A clone made this way cannot be
  re-pointed at a stock Fossil server and keep its shape; it heals to a
  content horizon or to dense. The zit draft states this; it is restated
  here because the exit guarantee of principle 7 is quarry's promise to
  keep.

## Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

- **Index plane, not the zit child.** The child could compute all three
  answers from the store, and against a stock zit server that is what the
  draft permits. quarry has the index and the §11 test; using them keeps the
  seed in agreement with the page and keeps the per-request cost an index
  read.
- **Splice at the front, not a second endpoint.** The seed and labels could
  arrive over the API in a separate request. They ride the sync reply
  because the draft specifies them as pragmas, so a zit client needs one
  code path for every origin, and because the labels must arrive with the
  manifests they qualify.
- **No seed storage.** A seed table would be a §1 derived-plane table with
  nothing to derive from except the index it duplicates. Minting on request
  is cheaper to keep correct than caching and invalidating.
- **Archives for runners, unchanged.** The horizon could have been offered
  to runners too. They are better served by the tree they already fetch,
  and offering a clone to a job that holds a deploy credential (RFC 0009,
  "a runner holding a secret runs one branch") would widen what the
  credential can reach.

## Prior art
[prior-art]: #prior-art

- Git's partial clone (`--filter=blob:none`) is the content horizon's shape:
  all commits, no old blobs, on-demand fetch. Git's shallow clone
  (`--depth`) is the structural horizon's shape without a seed: real commit
  hashes, parents recorded as absent in a side file, branch state carried by
  refs rather than derived. Fossil has neither; the sparse-cone draft gave
  zit the first and this gives it the second.
- Scion's thin client holds nothing it is not using and trusts a stateless
  server for every derived answer. The structural horizon lands between
  that and a dense clone: it trusts the origin for one control artifact and
  derives everything else from real artifacts it verifies by name.

## Unresolved questions
[unresolved-questions]: #unresolved-questions

- Whether `horizon-served` should name the history namespace root or the
  annotate route specifically. The draft says "an interface"; this RFC
  proposes the root and leaves the client to compose.
- Whether a repository may opt out of serving structural horizons the way
  it may opt out of verdict tags (RFC 0001 §6). Proposed: yes, a repository
  setting `horizon.structural: false`, default on, because a steward who
  does not want quarry's derivation standing in for history should be able
  to say so.

## Future possibilities
[future-possibilities]: #future-possibilities

- A horizon suggestion in the clone instructions quarry renders on a
  repository page: the common ancestor of the open leaves, recomputed per
  render, so a person copying the command gets a C that keeps every live
  branch's divergence held.
- Deepen as a page action: a horizon clone's `zit annotate` prints a URL;
  the page at that URL could offer the deepen command with the computed C.
