- Feature Name: `companions` (a repository's forum kept in a separate,
  fossil-compatible store, bound to its parent by the forge)
- Start Date: 2026-09-10
- Status: Draft for review
- Depends on: RFC 0001 §8 (OLTP is forge-local state, the DAG is truth),
  §9 (the ops seam); RFC 0004 (capability letters); RFC 0008 (roles);
  RFC 0011 (topics are per repository); the mirror machinery
  (`internal/oltp/mirror.go`, the mirror tick)
- Precedent: tickets already live beside the repository rather than in
  it (`local_ticket`); fossil's own project hosts its forum in a
  repository separate from its source, and many fossil projects do the
  same

## Summary

A fossil repository carries everything as artifacts in one DAG: code,
wiki, tickets, forum. quarry already keeps tickets beside the repository.
This RFC does the same for the forum, and for nothing else, in a way that
keeps the forum a real fossil repository rather than a table.

A **companion** is a fossil-compatible store that holds only forum
artifacts. A **binding** is a row in the forge's OLTP that says
repository X's forum is store Y. Everything a reader or a member touches
(`/r/X/forum`, grants, notifications, search) stays on X. Everything a
store needs (a lease, an index, a sync schedule, a place on the Fleet
page) belongs to Y.

Three decisions are settled here rather than left open:

1. **The wild pattern comes first.** A fossil project that already keeps
   its forum in a second repository is mirrored as two stores and bound.
   No split, no migration. This is Phase 1 and is useful alone.
2. **Companions are hidden from the repositories list and present on
   Fleet.** The list answers "what projects are here"; Fleet answers "is
   every store healthy and current", and a stale mirrored forum is a
   Fleet fact.
3. **Splitting is opt-in.** A new repository keeps its forum in the
   repository, as fossil does, until an admin splits it. A forge-wide
   setting lets an operator make separation the default for
   repositories created afterwards. The shipped default may flip later;
   it does not flip in this RFC.

The wiki stays in the repository. Embedded docs are versioned files in
the check-in tree and are outside this RFC entirely.

## Why the forum and only the forum

An in-repository feature pays three costs in proportion to how it is
used:

| cost | what pays it |
|---|---|
| every clone carries it for ever | anything high-volume |
| every write takes the repository's single-writer lease and refolds its index | anything written often |
| the writers are not the committers | anything a non-committer can write |

Tickets paid all three, which is why they moved. The forum pays all
three: it is unbounded, it is written by anyone holding a forum letter,
and nobody wants a project's support threads in their clone. The wiki
pays none: it is small, its authors already hold the lease, and a wiki
that travels with the clone is the reason to use a fossil wiki at all.
The binding table is general enough to carry `feature = 'wiki'` if a
project ever asks; this RFC does not build it.

## Why a store and not a table

The short route is a `forum_post` table in OLTP beside `local_ticket`.
It is refused. A forum that lives only in the forge is the one part of a
project that cannot be cloned, synced, backed up with the DAG or read by
a plain fossil client, and it would be quarry's first departure from
RFC 0001's rule that the forge holds nothing the DAG cannot carry.

A companion is a full repository. `fossil clone https://forge/r/X-forum`
works with any fossil client and brings only the conversation. Sync,
backup, the artifact format and the forum ops are what they are today.
The only new thing is the binding.

## The binding

```sql
CREATE TABLE IF NOT EXISTS repo_companion (
	repo    TEXT NOT NULL,             -- the parent, as in repo.name
	feature TEXT NOT NULL,             -- 'forum' (the only value this RFC defines)
	store   TEXT NOT NULL,             -- the companion, as in repo.name
	since   TEXT NOT NULL,             -- when the binding was made
	PRIMARY KEY (repo, feature),
	UNIQUE (store)                     -- a store is one repository's companion, or nobody's
);
```

Forge-local, like a grant (RFC 0001 §8). Wiping it loses wiring, never a
fact the DAG holds: the parent and the companion are each complete on
their own.

Rules the table enforces or the ops enforce:

- A companion is a row in `repo` like any other store. It has a path, a
  lease, an index, and if mirrored a `mirror` row with its own schedule.
- A companion is never itself a parent, and never bound twice.
- The parent's `visibility` governs the pair. A private project's forum
  is private; a companion has no visibility of its own.
- Nothing is inferred from names. `X-forum` prefills the suggestion on
  the settings page; an admin confirms it. A wrong automatic binding
  would put one project's forum under another.

## What the parent keeps

**Grants and letters.** The companion has no People page. Every forum
question — may this account post, reply, moderate, announce — is answered
by the parent's capability set (RFC 0004), read through the parent's
name. `capsFor(account, parent)` is the call; the companion's name never
reaches the gate.

**Routes.** `/r/X/forum` and `/api/v1/r/X/forum…` are unchanged. The
forum ops resolve X's store for forum purposes through the binding and
read or write there. A reader cannot tell and does not need to.

**Notifications.** Forum events publish on the parent's topic
(`repo-X`, RFC 0011). Subscriptions, digests, mentions and reply-by-email
(RFC 0014) are unchanged: a transaction id names the post hash, and the
receiver finds the post wherever the store is.

**Search.** The parent's search includes its companion's threads, ranked
as today. The companion is not searchable on its own name.

**Activity.** Forum activity counts toward the parent's profile and
timeline, attributed to the parent.

## What the companion gets

- **Its own lease and index.** A busy thread never waits on a push and a
  push never waits on a thread.
- **No CI.** The registrar treats a companion as a store with no
  check-ins: nothing triggers a pipeline, and ingest spends nothing on
  it beyond the forum index. The `ci_run` and watermark machinery does
  not see it.
- **Its own sync.** A mirrored companion has its own `mirror` row,
  schedule, last-success and failure count. Its staleness is its own
  fact.
- **A place on Fleet and Stores**, shown with its parent's name beside
  it, so "X-forum is 3 days behind" reads as "X's forum is behind".

## Where it appears and where it does not

| surface | companion shown? | why |
|---|---|---|
| `/` repositories list, `repo.list` | no | the list is projects; a forum is not a project |
| `/r/X-forum` direct | redirects to `/r/X/forum` | one address for one thing; the clone URL still works |
| Fleet, Stores | yes, with parent | health and freshness are per store |
| X's settings page | one line naming the store | the binding is an admin's decision and must be visible |
| Mirrors page | yes, as a mirror | it is one |
| search index | under X | see above |

`repo.list` gains a `companions` field on the parent rather than a row
per companion, so an API client that wants them has them without the
list changing shape for everyone else.

## Operations

All three are `ops` on the parent (RFC 0001 §9), admin-only on that
repository, and each is idempotent.

**`forum.bind`** — `{repo, store}`. Binds an existing store as X's
forum. Refuses if the store is a parent, already bound, or holds
history (a store with check-ins beyond fossil's initial empty one is
a project, not a forum; the fossil forum itself has exactly that one). This is the wild
pattern: mirror `fossil`, mirror `fossil-forum`, bind.

**`forum.split`** — `{repo}`. Creates `X-forum` beside X (an empty
store, same node, same visibility), binds it, and from then on new posts
go there. Forward-only: existing threads stay in X and stay readable.
Refuses on a mirror; a mirror is somebody else's history and its forum
arrives with it.

**`forum.fold`** — `{repo}`. Removes the binding; new posts go back to
X. The companion store is left in place, unbound, and its threads stay
readable until an admin deletes the store. Forward-only for the same
reason.

While a parent holds both old in-repository threads and a companion,
the forum page lists both, oldest from the parent and newest from the
companion, as one list with no seam a reader has to understand. The
reply DAG never crosses stores: a reply to an old thread lands in the
store that holds the thread.

## Default and the forge-wide setting

New repositories keep their forum in the repository. This is fossil's
default, it is the shape a person bringing a fossil repository expects,
and it is the shape that is cheapest to be wrong about: the split is
forward-only, so a default of "separate" is sticky for everyone while an
opt-in mistake is sticky only for the one who chose.

One forge-wide setting, off by default: **new repositories keep their
forum separate**. When on, `repo.new` creates and binds a companion.
Repositories that already exist are never touched by the setting.

The shipped default is a later decision, taken after split and fold have
run on real projects, and it changes only repositories created after the
flip. Nothing in this RFC forecloses it.

## What is not proposed

- Moving the forum, or any artifact, into OLTP.
- Splitting the wiki by default or on request. The table can carry it;
  nobody has asked.
- Any change to embedded docs.
- A separate identity, grant or visibility model for the companion.
- Moving history. See below.

## Migration is later, and it is honest about clones

A one-shot copy of a parent's forum history into its companion is
possible: forum posts reference only other forum posts, by hash, so the
artifacts move intact and their hashes survive. What does not survive is
their presence in every existing clone of the parent. Fossil has no way
to un-clone an artifact, so a moved forum remains in the history of
anyone who cloned before the move. That is acceptable and should be
said plainly rather than promised away. It is a later phase, built only
if a project asks.

## Phases

Each ships alone and is useful alone.

**Phase 1 — bind.** The `repo_companion` table, `forum.bind`, forum ops
that resolve the store through the binding, the registrar's no-CI
treatment of a companion, the parent's settings line, the repositories
list hiding companions, Fleet and Stores showing them with their parent.
Proves the design on the wild pattern with no new store created by the
forge.

**Phase 2 — split and fold.** `forum.split` and `forum.fold`; the forum
page reading two stores as one list; the redirect from `/r/X-forum`.

**Phase 3 — the forge-wide setting.** `repo.new` honouring it.

**Later — history copy**, if wanted.

## Open questions

1. **Deleting a parent.** Does deleting X delete its companion, or
   unbind and keep it? Keeping it is safer and leaves a store nobody can
   reach except by clone; deleting it is what a person probably meant.
   Proposal: delete, after the same confirmation the parent requires,
   and say so in the confirmation.
2. **Naming.** `X-forum` is the convention the wild uses and the one the
   settings page prefills. Should the forge refuse any other name for a
   companion it creates, or only suggest? Proposal: the forge creates
   only `X-forum`; `forum.bind` accepts any name because the wild has
   many.
3. **Moderation staging.** Staged posts (`maybeStage`) are held in the
   forge before they become artifacts. They are held against the parent
   today; they should release into whichever store the binding names at
   release time, not at staging time.
