Skip to content
quarry Artifact [47f435852f]
Login

Artifact [47f435852f]

Artifact 47f435852f517f25905a197db5811289905fe2ced50ddbc17fb9700660584d2f:


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

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:

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

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

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.