- Feature Name: `strata` (stacked landings, rendered as a chain)
- Start Date: 2026-08-24
- Status: Implemented
- Depends on: RFC 0001 §4 (event bus), §6 (CI); `docs/landings.md` (the record
and its lifecycle); RFC 0004 §4 (capability model)
- Upstream: zit#144 — the CLI half. This RFC covers only what the forge adds.
- Findings, with the experiments that produced them:
https://claude.ai/code/artifact/69ab2f7e-c60f-4cb3-ae8e-7da871a399cb
## Summary
[summary]: #summary
A **stack** is a chain of dependent landings: each segment targets the branch
below it, and they land bottom-up. quarry renders the chain, refuses accept on
anything that is not the bottom, and flags a segment whose base was abandoned.
The name is **strata** — layers deposited in order, each resting on the one
below, which is what the data already is.
**No new index and no new artifact.** quarry computes landings as a reading of
forum posts (`docs/landings.md`); a stack is a reading of those same landings.
The edge is already there: a landing whose `target` equals another open
landing's `branch` *is* a stack edge. Nothing needs a `depends:` field.
## Why this is small here
[why]: #why
Every stacked-PR tool in git automates a rebase. GitHub's native stacked PRs
(public preview 2026-07-30) rebase and retarget the whole stack server-side when
the bottom merges. That is the product and also the cost: force-pushes,
comments stranded as "outdated", cascading conflicts, CI churn.
zit cannot rebase, and does not need to. When a segment lands as a *merge*, the
segment above it still has the lower tip as an ancestor, so a later three-way
sees only its own delta. Measured against zit trunk `944ee007`, including the
case that forces a restack in git — two segments editing the same file:
=== after A landed, trunk shared.txt ===
A-EDIT / line2 / line3 / line4 / line5
=== featB -> trunk probe (post-land, same file) ===
merges clean
So the forge's "restack" is **not a history operation**. It is one metadata
write per child, and zit#144 puts it behind `landing accept`. quarry's job is
to show the chain and to refuse the out-of-order action.
This also means the comment problem never arises. A landing names a *branch*,
not a frozen range, and comments are replies in its thread — so there is nothing
commit-anchored for a restack to detach. That is a property worth stating
because it is the single loudest complaint about stacked PRs elsewhere.
## What quarry adds
[what]: #what
### The chain, on the landings page and the record page
A landing that is part of a stack renders its position: what it sits on, what
sits on it, and how deep the stack is. The list view groups a stack under its
base rather than interleaving segments with unrelated landings.
Derived, not stored: follow `target` → `branch` across open landings. A cycle is
possible to express and must be handled as a rendering fault, not a crash —
report the chain as broken and name the segments involved.
### Rendering the chain — the proposal
Written after the first real strata on this forge (RFC 0007, five segments)
landed in the list as five open landings that happened to name each other.
Nothing said they were one thing. A reader saw *5 Open*, five rows in
opened-order, and had to read the branch chips to reconstruct that
`protect-5 → protect-4 → … → trunk` was a stack with exactly one landable
end. That is the failure this section exists to name: **a strata that looks
like five independent landings is worse than no strata feature**, because
the list actively misleads about what can be acted on.
#### What the field does
Three products have solved this, in three shapes, and the shapes agree more
than they differ.
- **GitHub** (native stacked pull requests, public preview 30 July 2026):
in the pull-request list each member carries a position badge — `1/3`,
`2/3`, `3/3` — so the eye reads one ordered feature; on a pull request a
*stack map* sits at the top showing every layer with its review status,
highlighting the current one, one click to any other. The merge box shows
the same map. Members still appear as individual rows.
- **Gerrit**: the *Related Changes* panel on every change lists the whole
*relation chain* — what this depends on below, what depends on it above —
titles as links. The list view does not group; the change page carries
the structure. *Submit Whole Topic* lands a chain in one action when every
member is submittable.
- **Graphite**: the stack is the first-class object. The web app renders a
stack as one card with its layers, and each layer's page shows the stack
rail for navigation.
The common ground: a **position** on every member (`n/N`), a **map** on the
record page with the current layer marked and one-click navigation, and
**one action at the landable end**. Where they differ is the list — GitHub
and Gerrit keep rows and add a badge; Graphite groups.
#### The proposal
quarry groups, because its list is a *queue* and a queue that shows five
rows for one decision counts wrong. The `5 Open` chip in the screenshot is
the tell: there was one thing to decide, not five.
**In the list.** A stack is **one row**, drawn at the position its base
would take and carrying the base's title, with a strata mark beside the
state glyph and a summary line: *strata · 5 segments · `protect-1 → trunk`
landable*. The row expands (a `<details>`, no script) to the segments,
bottom-up, each with its position badge `1/5` … `5/5`, its own branch
chips, and its own state glyph, so a segment already accepted (the stack
is being walked) reads as done while the rest wait. Not a separate
"Strata" section: the queue stays one queue, and a stack is a member of it
that happens to be tall. The count chip counts **decisions**, so a stack of
five counts one.
**On the record.** A stack map above the fold, the GitHub shape: every
segment in order, the current one marked, each with state and a link; the
landable segment named as such. Below the bottom the accept control is
disabled with the reason, as the RFC already says; the map is *why* the
reason makes sense at a glance. A segment whose base was rejected shows
the map with the broken rung marked — §An abandoned base is flagged,
drawn rather than only said.
**On the landings page filter.** No new chip for now. `Open` already means
"awaits a decision", and a grouped stack is one open decision. If stacks
turn out to need their own listing, the chip is *Strata* beside *All*, and
it filters to grouped rows — a listing question, not a model one, and not
this layer's.
**What makes it a strata, mechanically.** Unchanged from above: follow
`target` → `branch` across *open* landings. A landing whose target is the
branch of another open landing is a segment; the one whose target is not
is the base. Position is depth from the base. A cycle is a rendering fault
and the row says *broken chain* and names the members. A settled segment
leaves the chain when the next one is retargeted (zit#144), which is what
makes "1 of 5 landable" move up the stack as it is walked.
#### Not proposed here
*Land the whole strata* — Gerrit's *submit whole topic*. zit accepts one
segment at a time and retargets the next; quarry could sequence five
accepts bottom-up when the viewer may land every one of them. Real, wanted,
and a separate change: it is an action with a failure mode in the middle
(segment 3 conflicts after 1 and 2 landed), and that failure mode needs its
own design before the button exists.
#### Build order
| phase | work |
|---|---|
| 1 | `landing.list` derives chains: base, position, depth, broken; the count chip counts decisions |
| 2 | the grouped row with `<details>` segments and position badges |
| 3 | the stack map on the record page; rejected base drawn |
| 4 | harness: a five-segment stack lists as one row, expands to five, counts one; accepting the base moves the landable mark |
### Accept is disabled above the bottom
Only the bottom of a stack is acceptable. quarry disables the control and states
the reason, in exactly the posture it already uses for a conflicted landing:
**a control that would fail is not offered.**
The refusal is zit's too (zit#144), so the rule holds for a CLI accept as well.
quarry disabling the button is the courtesy; the guard is upstream.
### An abandoned base is flagged
Reject the bottom and every segment above it is stacked on work nobody will
land. Those segments still report `merges clean` against their target, which is
true and useless. The record page says the base was rejected and names it.
### Events
Per RFC 0005's noun-and-action vocabulary, the stack needs no new nouns. A
retarget is a change to an existing landing, so it rides `landing.updated`;
opening and settling already have events. A pipeline that wants "the stack
advanced" subscribes to `landing.accepted` on the base and reads the chain.
Adding `landing.retargeted` is tempting and probably wrong: it describes a
mechanism, not a thing that happened to a repository, and RFC 0005's test for a
noun is that a person would recognise it.
### Capabilities
Unchanged. Accept still requires fossil's `i` (Write/check-in), because it
writes a merge check-in. Retarget writes a forum reply, so it requires `3`
(WrForum) like every other landing transition. No new capability letter, which
matters — RFC 0004 §4 is explicit that inventing one is quarry policy wearing
fossil's clothes.
## What this is not
[not]: #not
- **Not a merge queue.** The ordering guard plus the existing mergeability probe
covers sequencing at cathedral scale. A queue answers contention that does not
exist here.
- **Not a rebase engine.** Nothing in quarry rewrites history, and the whole
advantage of this model is that it does not have to.
- **Not a new index.** Landings are already a reading of forum posts; a stack is
a reading of landings.
- **Not speculative mergeability.** "Would this merge into trunk once its base
lands?" is not exactly answerable before the base lands, and once zit
auto-retargets it becomes the ordinary question the existing probe answers
exactly. Rendering an approximation would be a confident wrong answer.
## Open questions
[open]: #open
- **Concurrent accepts.** Two people accepting adjacent segments at once, on a
bare store. The upstream ordering guard reads state and then acts; the race
window between them is unexamined, and quarry is where it would be observed.
- **Depth.** Everything above was measured on a three-deep stack. The merge
property is structural rather than empirical, but nothing beyond three has
been run.
- **Fold cost.** The chain is derived per view. On a store with many open
landings this is a walk over all of them; whether it needs memoising on the
same tip-pair key the mergeability answer already uses is unmeasured.
- **A stack spanning repositories.** Out of scope here, and probably meaningless
— a landing assumes the branch is already in the store.
## The cost worth stating
[cost]: #cost
fossil never squashes and never rebases, so a deep stack leaves every
intermediate merge in permanent history — landing merges plus a restack merge
per revision of each base. Trunk stays correct and becomes visibly braided.
That is the trade this model makes: noisier history in exchange for never
rewriting, never force-pushing, and never stranding a review comment. It is the
right side of it, but the cost grows with depth in a way git's does not, and
history legibility — not correctness — is where a complaint would surface first.