- Feature Name: `namespaced-repositories` (a repository is named within its
  namespace, a person's or a team's, not across the forge; a permanent ID
  is its key and namespace/name is its address; flat names stay a
  deployment option for single-user builds)
- Start Date: 2026-10-21
- Status: Draft — decisions settled by the owner on 2026-10-21 (see
  [Decisions](#decisions)); rungs 1 (schema), 2 (addresses) and 3 (creation per namespace, names on pages) built
- Depends on: RFC 0001 §2 (store access), §7 (sync fronting), §9 (the ops
  layer generates the API); RFC 0010 (local mode); RFC 0015 (companions);
  RFC 0018 (teams: the shared pool of account and team names, the
  namespace a repository appears under, repo_place_past)

## Summary
[summary]: #summary

RFC 0001 chose a flat repository namespace, and it is still flat underneath.
RFC 0018 gave every repository a namespace, the person or team it appears
under, and `/r/dmestas/quarry` resolves; but the name after the namespace is
still the forge-wide key of the repository. There can be one `tools` on the
whole forge. Every person and team competes for every name, and a forge that
grows runs out of good ones.

This RFC splits a repository's **key** from its **address**. The key is a
permanent ID that every table, store path, route and record refers to, and
that never changes. The address is namespace/name, unique within its
namespace: `dmestas/tools` and `some-team/tools` are two repositories.
Moving a repository between namespaces (a person to a team, a mirror to its
steward) or renaming it changes one row; nothing keyed by it is rewritten.

Every existing repository's ID is its current name. Those names are unique
today, so every column that holds one, every store path, and every clone
URL already in use stays valid with no data rewritten. Only repositories
made after this lands get generated IDs.

Flat names remain a deployment option: a single-user build (`quarry ui`, and
the WebAssembly build that will run as `zit ui`) has one person and no
namespaces, and addresses repositories by name alone.

## Decisions
[decisions]: #decisions

Settled by the owner on 2026-10-21:

1. **The key is a permanent ID**, not the address string. A move or a
   rename touches one row, and no grant, link, CI record or store path has
   to follow it.
2. **A bare `/r/<name>` redirects when it is unique.** Exactly one
   repository with that name: redirect to its namespaced address, so every
   link saved so far keeps working. Several: a short page listing them.
   New links always carry the namespace.
3. **Forge-held repositories live in one forge namespace, `zeitforge`.**
   The mirrors nobody owns (tcl, tk, sqlite, fossil, netbsd-src, pkgsrc and
   the rest) appear as `zeitforge/tcl`. The name is reserved: no account or
   team can take it. A mirror may later be **stewarded** by a person or
   team; it then moves into their namespace like any other move, and its
   `zeitforge/` address keeps redirecting.
4. **Teams are namespaces exactly as people are.** A team's slug scopes
   names the way a person's handle does. The deed stays with a person
   (RFC 0018: dissolving a team never strands code); this RFC does not
   change who holds the deed.
5. **Namespacing is a deployment option.** Flat names for single-user
   builds; namespaced for a forge.

## Design
[design]: #design

### Key and address

The `repo` table's `name` column stays the key column, so none of the 66
columns across 49 files that refer to a repository change type or meaning:
they hold the ID. Two columns carry the address:

- `namespace` (exists since RFC 0018): an account handle, a team slug, or
  `zeitforge` for a forge-held repository.
- `handle` (new): the repository's name within its namespace.

`UNIQUE (namespace, handle)` replaces the forge-wide uniqueness of the
name. For every row that exists when this lands, `handle = name`.

A new repository's key is its name, unless that key is taken or could be
read as something else first in a path (an account, a team, a reserved
route, `zeitforge`); then it is minted, `rp-` and eight letters, a prefix
the account and team name rule refuses, so a key is never read as a
namespace. Settled by the owner on 2026-10-22: nearly every repository's
key stays readable, and only a clash shows a minted one wherever a page
prints the key. A reader is meant to see the address.

A new store is created at `<repos-root>/<id>`. Existing stores keep the
path their row already records.

### Addresses on the wire

Routes are not rewritten. The forge already reads `/r/<namespace>/<repo>`
before its routes match (RFC 0018); that reading now resolves the pair to
the ID, and the 173 web routes and 164 API routes continue to receive
`{repo}` as they do today. The same resolution applies to:

- **web pages:** `/r/<namespace>/<handle>/...`
- **the API:** `/api/v1/r/<namespace>/<handle>/...`
- **sync:** `fossil clone` and `zit clone` of `/r/<namespace>/<handle>`
- **links the forge writes:** every link is built from the address, never
  the ID (the `repoURL` helper already builds namespaced links)

A first segment is read as a namespace when the second segment is a
repository in it, and otherwise as a repository: the pools stay disjoint as
RFC 0018 requires, now between namespaces and IDs rather than namespaces
and names.

### The bare form

`/r/<x>` with no namespace resolves in this order:

1. `x` is an ID: that repository. Every existing repository's ID is its
   name, so every remote cloned from `/r/quarry` keeps syncing forever,
   whatever is created later.
2. `x` is the handle of exactly one repository: redirect a page to its
   address; answer sync and the API as that repository.
3. `x` is the handle of several: a page listing them. Sync and the API
   are refused with a message that names each namespaced address.
4. Otherwise: not found.

Moves already redirect through `repo_place_past` (RFC 0018); a rename adds
the old handle to the same memory, so an address that was ever valid keeps
leading to the repository that held it.

### Creating a repository

The new-repository form gains **Owner**: the reader, or one of their teams
where they may create. The name is checked within that namespace as it is
typed (#979 checks it forge-wide; this narrows the check), and an agent
creating a repository places it in its person's namespace, where the deed
already goes (#969).

### Flat mode

`quarry serve --repo-names=flat|namespaced`, namespaced by default. In flat
mode the address is the handle alone, `UNIQUE (handle)` holds forge-wide,
links are `/r/<handle>`, and the form has no owner field. `quarry ui` and
the WebAssembly `zit ui` build run flat. The key and the stores are the
same in both modes, so a repository moves between a flat build and a forge
without changing anything but how it is addressed.

## Migration
[migration]: #migration

Each rung lands on its own and leaves the forge working.

1. **Schema, no behaviour change.** Add `handle`, backfilled from `name`
   and written by every insert; add `UNIQUE (namespace, handle)` beside the
   existing uniqueness; reserve `zeitforge` for accounts, teams and
   repositories alike. (Built.)
2. **Addresses resolve to IDs.** The `/r/<namespace>/<handle>` reading,
   the API and sync forms, the bare-form rules and the listing page
   (`/named/<handle>`); every link the forge builds is the address.
   (Built.) Rename memory waits for renames, which do not exist yet.
3. **Creation is per namespace.** (3a built.) The owner field (the
   reader, or a team they manage, the rule `team.repo.add` keeps); the
   name check within the namespace, covering the address a private
   team's repositories show under; keys as above, with the store at
   `<repos-root>/<key>`. Every way a repository comes to appear
   somewhere refuses a second one at the same address: joining a team,
   a team going private or being dissolved, a deed changing hands (asked
   before the deed moves); the start-up repair leaves a repository where
   it is rather than collide; the name check covers the owner's
   namespace for a private team's repositories, which appear there.
   3b (built): pages say a repository's handle where they printed its
   key (`repoName`), attributes the server reads back keep the key, a
   page title built from the key says the handle, and an API example on
   a page is built from the address. Text composed outside a page
   (mail, notifications, event summaries, logs) still says the key; it
   differs only for a minted one.
4. **Mirrors move to `zeitforge/`.** Stewardship is a move into the
   steward's namespace. The start-up repair that holds a namespace to its
   team or owner (`repoMigrations`) learns that a repository with no owner
   appears under `zeitforge`; as it stands it would clear that namespace
   on the next start.
5. **Flat mode** for `quarry ui` and the WebAssembly `zit ui` build, and
   `--repo-names=flat|namespaced` with it: the switch arrives when there
   is something for it to switch.

## Not in this RFC

- **Renaming as a feature.** The key makes it one row and the rename memory
  is specified above; the control that offers it is separate work.
- **Team-held deeds.** Who holds the deed is RFC 0018's rule and stays as
  it is.
- **zit's own naming.** A zit store does not know the address it is served
  under, so nothing in zit changes.
