§22 S6: merge three-tier model into SPEC.md + registry format in DEPLOYMENTS

Write the canonical §22 (deployment → project → RFC collection) into the
binding spec, applying Part A of the design doc and the Part D amendments in
place. Lands the S3 keystone reinterpretation of §B.1/§B.3: a plain granted
account is a granted account, not a global write role; "global RFC Contributor"
is an explicit memberships(scope_type='global') grant; the implicit-public
write baseline is grandfathered onto the migration-seeded default collection
only (N=1 preserved).

- SPEC.md §22.1–§22.14: tiers + isolation, the registry + .collection.yaml
  manifests, content-repo-per-project, per-collection slug identity, collection
  type/initial_state/unreviewed, two-tier visibility (narrow-only), the unified
  {owner, contributor} role vocabulary at {global, project, collection}, the
  four-layer most-permissive union, discovery/joining (invite + request-to-join),
  runtime branding, /p/<project>/c/<collection>/ routing, one inbox,
  per-collection model universe, the default-project+collection migration, and
  §22.14 consolidating the §§1–21 amendments.
- Forward-pointer amendment notes (house style) at §1, §2, §5, §6 routing
  readers to §22.
- docs/DEPLOYMENTS.md: the registry (REGISTRY_REPO / projects.yaml) and
  .collection.yaml manifest formats, the N=1 default-project+collection upgrade,
  and in-app create-project/collection.

Per docs/design/2026-06-05-three-tier-projects-collections.md Parts A/B/D/E (S6).
Docs-only; no code or migration. Releases as part of v0.45.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-06 01:21:39 -07:00
parent b0737380cd
commit 26f3680197
2 changed files with 618 additions and 0 deletions
+93
View File
@@ -90,6 +90,99 @@ the framework at that pin against your Gitea; the framework knows
nothing about your specific deployment beyond what your `.env`
files told it.
## The registry: projects and collections (§22)
From **v0.45.0** a deployment is **three tiers** — deployment →
**project** → **RFC collection** — instead of one corpus. Most of
the single-corpus setup above still applies (a deployment that wants
one corpus runs the **N=1 case** unchanged), but two things move into
**git-truth the framework mirrors**, exactly the way RFC bodies do.
The binding model is [`SPEC.md` §22](../SPEC.md); this is the operator
view of the file formats.
### The registry repo (`REGISTRY_REPO`) declares projects
`VITE_APP_NAME` and `META_REPO` are superseded. The framework now reads
a **registry repo** — a dedicated repo under your Gitea org, named
whatever you like, whose location you pass in `backend/.env` as
`REGISTRY_REPO` (the framework fails loudly at startup if it is unset).
Its root holds a `projects.yaml`:
```yaml
# projects.yaml (registry repo root)
deployment:
name: Wiggleverse # deployment display name (was VITE_APP_NAME)
tagline: A substrate for collaborative standardization
projects:
- id: ohm # url-stable slug, unique in the deployment → /p/ohm/
name: Open Human Model
content_repo: ohm-content # ONE repo under your org; collections live inside it
visibility: public # gated | public | unlisted (§22.5)
theme: { accent: "#5b5bd6" } # optional per-project token overrides
enabled_models: [claude, gemini] # optional; falls back to ENABLED_MODELS
```
Each project owns **exactly one content repo**. Adding, reconfiguring,
or archiving a project is a PR against `projects.yaml`; the framework's
webhook + reconciler mirror it into the `projects` cache table. Project
**membership** is app state (it churns at user speed), not registry
data. The deployment's display name and tagline come from here, served
at runtime via `GET /api/deployment` — no rebuild needed to rename.
### The content repo declares collections via `.collection.yaml`
A project's collections are **typed subfolders** of its content repo,
each carrying a `.collection.yaml` manifest the registry mirror reads:
```
ohm-content/
model/
.collection.yaml # type: document
rfcs/intro.md
specs/
.collection.yaml # type: specification
rfcs/runtime.md
features/
.collection.yaml # type: bdd
rfcs/login.md
```
```yaml
# ohm-content/model/.collection.yaml
type: document # document | specification | bdd — IMMUTABLE once set
visibility: gated # defaults to the project's; may only NARROW it
initial_state: super-draft # super-draft | active — defaults from type
name: The Model
# enabled_models: [claude] # optional; may only narrow the project's universe
```
`type` is fixed at creation (the framework refuses to change it on a
later mirror). `visibility` may be as strict as or stricter than the
project's, never looser (`public` < `unlisted` < `gated`); reading or
writing a collection requires passing **both** the project and the
collection gate. `enabled_models`, if present, narrows the project's
model universe for that collection.
### Default project + default collection (the N=1 upgrade)
A deployment upgrading from a pre-§22 version is migrated automatically:
its single corpus becomes one **default project** carrying one **default
collection** (`id` `default`, subfolder = repo root), inheriting the old
`type` / `initial_state` / visibility. Old URLs 308-redirect into the
`/p/<project>/c/default/…` form, so existing links survive. Until you
add a second project or collection the deployment is functionally
identical to before, with one extra path segment. The ordered upgrade
actions are the [`CHANGELOG.md`](../CHANGELOG.md) entry's upgrade-steps
block (§20.4).
### Creating projects and collections in-app
Both tiers can also be created from the UI by an Owner — the action
wraps a bot commit (a new content repo + `projects.yaml` entry for a
project; a new subfolder + `.collection.yaml` for a collection), so
everything still flows from git. Nothing becomes app state the mirror
cannot rebuild.
## The two-repo working pattern
Once a deployment is live, day-to-day changes split across the