# Draft spec — §22 Multi-project deployments + amendments + slicing plan > Status: **draft for review.** Binding voice, but not yet merged into > `SPEC.md`. When accepted: §22 below is appended after §21; the amendment > notes in Part B are applied in place; the slicing plan in Part C seeds a > new `docs/DEV.md` build section. Rationale and the decisions behind this > live in [`multi-project.md`](./multi-project.md). Target release: the next > minor (a pre-1.0 minor carrying breaking changes with upgrade steps, §20.2). --- # Part A — New canonical section ## 22. Projects: multiple corpora per deployment A **deployment** hosts one or more **projects**. A project is a single corpus: one content repository (§1) holding entries under `rfcs/`, with its own per-project slug namespace (the slug is the identity — §22.4), a declared **type** (§22.4a), catalog, philosophy, branding, member roster, and model universe. The deployment is the substrate the projects share — one Gitea org, one bot, one account system, one inbox, one running process — and the surface a visitor first lands on. Everything §§1–21 describe about *a corpus* is now *a project*. Everything they describe about *a deployment* that is not corpus-specific — accounts, the §6 admission gate, the §15 inbox, the §1 bot — stays at the deployment level and is shared across projects. The numbered sections that assume a single corpus are amended in Part B; §22 is the binding model they defer to. > **Multi-project change (target: next minor — supersedes the original > single-corpus model).** §1 originally said "for a deployment, this single > repository is its content repository." A deployment now has a **registry** > (§22.2) naming N content repositories, one per project. The single-corpus > deployment is the **N=1 case** and continues to run after migration via a > generated default project (§22.13); no deployment is forced to adopt more > than one project. Where earlier sections say "the meta repo" or "the > corpus," read "the project's content repo" and "the project's corpus." ### 22.1 The deployment ⇄ project relation One deployment, N projects (N ≥ 1). A project belongs to exactly one deployment and never moves between deployments. Projects within a deployment are isolated by default (§22.5): an RFC, branch, thread, star, or watch belongs to exactly one project, and no app surface joins across projects except the per-account ones the deployment owns (the §15 inbox, the §6 account roster, sign-in). ### 22.2 The registry — git is still truth Which projects exist, and their configuration, is declared in git, mirrored into a `projects` cache table the same way content is mirrored into `cached_rfcs` (§4). The registry is a file the bot reads — a `projects.yaml` at the root of a dedicated **registry repo** under the deployment's Gitea org. The framework learns the registry repo's location from a required env var (`REGISTRY_REPO`, the multi-project successor to `META_REPO`); the repo's *name* is the deployment's choice, not the framework's, per the separation-of-concerns rule, and the framework fails loudly at startup if the var is unset. Adding, reconfiguring, or archiving a project is a PR against that file; the §4 webhook + reconciler keep the `projects` table in sync, recording the merged `registry_sha` on each row for provenance. The registry is a **deployment-side repo the framework reads**, in exactly the sense `META_REPO` is today — not operator-tooling config. Where a deployment is assembled by an external operator tool, that tool supplies the `REGISTRY_REPO` value in the deployment's `.env` (as it supplies `META_REPO` now) and is otherwise unaffected: project definitions live in git, edited by PR, and the framework knows nothing about the tool that wrote the env var. ```yaml # projects.yaml (registry repo root) deployment: name: Wiggleverse # deployment display name (replaces VITE_APP_NAME) tagline: ... # deployment landing deck (§22.10) projects: - id: ohm # url-stable slug, unique within the deployment name: Open Human Model type: document # document | specification | bdd — immutable (§22.4a) content_repo: ohm-content # repo under the deployment's Gitea org (§22.3) visibility: gated # gated | public | unlisted (§22.5) initial_state: super-draft # super-draft | active — landing state of a new # entry; defaults from type (§22.4b) enabled_models: [claude, gemini] # optional; falls back to deployment ENABLED_MODELS theme: { accent: "#5b5bd6" } # optional per-project token overrides (§22.9) ``` Project **definition and configuration** live in the registry (git). Project **membership** lives in the app db (§22.6) — it churns at user speed and is app state, not document state, exactly as `rfc_collaborators` is (§5). `projects` rows are never written from user actions; they flow from the registry mirror only. ### 22.3 Content repositories — one per project Each project names one content repo under the deployment's single Gitea org (naming convention `-content`). The §1 bot service account operates org-wide across every content repo and the registry repo; nothing about the bot, the §6 app-owned authorization, or the "app is the only contribution surface" stance changes. There are no per-project Gitea orgs and no per-project bot accounts. ### 22.4 The slug namespace is per-project; the slug is the identity An entry's slug (§2) is unique **within its project**, not across the deployment: `ohm` and `specs` may each have an `intro`. The slug **is** the identity — a fully qualified reference is `(project_id, slug)`, and nothing more. There is no type prefix and **no per-project numeric ID**: within a project the slug alone is unambiguous, and the project context (its `/p//` URL prefix and chrome) carries everything the old `RFC-NNNN` label used to. This retires the per-project numbering of earlier drafts. The §13 graduation flip still happens — it moves an entry from proposal to graduated state and into the content repo — but it no longer allocates a number; the `max(integer IDs)+1` allocator (§2.3, the old `api_graduation.py` path) is removed. The displayed *noun* around a slug ("RFC", "Spec", "Feature") is a presentation concern driven by the project's type (§22.4a), not part of the identity. **Legacy numbers.** Entries graduated *before* this change keep their existing `id` (`RFC-NNNN`) in frontmatter as a **frozen, non-identity legacy label** — preserved and shown in the UI so external "RFC-0001"-style citations still resolve, but never used for routing or lookup (the slug is). New entries are never assigned one, and graduation does not write `id`. The field is read-only provenance from here on. ### 22.4a Project type Every project declares a `type` in the registry (§22.2), chosen at creation and **immutable**: one of `document`, `specification`, or `bdd`. Type does not change the engine — every type uses the same content repo (§22.3), the same propose→branch→PR→discuss→graduate lifecycle (§§9–13), the same threads, flags, and chat. Type selects exactly three things: 1. the **entry frontmatter schema** the project validates entries against (§2); 2. the **terminology** the chrome uses for an entry (the §8.1 noun, catalog labels); 3. the set of **type-specific surfaces** layered on top of the shared §7 catalog. Type-specific behavior is implemented as a per-type module the framework selects on `project.type`; the engine itself treats every entry as markdown + frontmatter regardless of type. `type` is an **open set** in shape — a future type is a new module plus a new allowed enum value, no schema rebuild — and the three below are what is defined now. The type names and their behavior are framework concepts (like role names), not deployment content: a deployment picks which type each project is, but does not define or rename types. - **`document`** — long-form normative prose (OHM: a model of principles and definitions). Frontmatter is the §2 baseline (title, status, owners/arbiters, tags). No type-specific surfaces. The §22.13 generated default project is a `document` project, so the N=1 case is unchanged. - **`specification`** — a versioned technical specification (this framework's own `SPEC.md` is the archetype: numbered normative sections, upgrade steps). Frontmatter adds spec metadata (`version`, lifecycle `status` of draft/active/superseded, `supersedes`). **Type-specific surface — release planning:** group entries/changes into versioned releases, carry a changelog + §20-style upgrade-steps per release, and surface "what is in the next release." - **`bdd`** — behavior-driven feature specs: each entry states a feature as Given/When/Then scenarios with acceptance criteria. Frontmatter adds feature metadata and an optional link to the `specification` entries a feature verifies. **Type-specific surface:** a scenario/acceptance view, and — where a deployment runs a BDD project alongside a specification project — a coverage view mapping features to the spec sections they exercise. > **Draft note.** The shared-engine boundary is locked; the per-type > *schemas and surfaces* above (notably the specification release-planning > data model and whether BDD scenarios are free-form markdown or a parsed > Given/When/Then structure) are first proposals, to be pinned in the > type-surface slice (Part C, M5). ### 22.4b Initial state of a new entry A project sets the **landing state** a new entry takes when its creating idea-PR merges (§2.4) — the `initial_state` registry field, one of the §2.4 super-draft entry-states: - **`super-draft`** (default for `document` and `specification`) — a new entry lands as a super-draft and must be explicitly graduated (§13) to reach `active`. This is today's flow: propose → super-draft → review → graduate. - **`active`** (default for `bdd`) — a new entry lands `active` on the idea-PR merge, with the **`unreviewed` flag set** (§22.4c). The §13 graduate gate is not surfaced — the entry is already active — but because nothing reviewed it, the flag marks it as not-yet-vetted until an owner clears it. A behavior spec is captured fact, not a proposal under deliberation, so it skips the review-then-promote ceremony; branches, PRs, and discussion work afterward exactly as on any `active` entry. The default comes from the project's **type** (§22.4a) — each type module supplies it — but `initial_state` is an independent registry knob: a deployment may run a `bdd` project with `super-draft` if it wants a review gate, or (less commonly) a `document` project that auto-actives. It changes only the *landing state and whether graduation is required*; the underlying propose→branch→PR→discuss engine is unchanged (shared-engine rule, §22.4a). The §22.13 default project keeps `super-draft`, preserving the N=1 flow. ### 22.4c The `unreviewed` flag An `active` entry carries an **`unreviewed`** boolean, orthogonal to its `state`. It records whether a human gate has vetted the entry: - An entry that reaches `active` by the normal **graduate** path (§13) is **never** flagged — the graduate action, taken by an owner/admin, *is* the review. - An entry that **skips straight to `active`** via `initial_state: active` (§22.4b) lands with `unreviewed = true`, because nothing reviewed it. A **project owner** — `project_admin`, or a deployment `owner`/`admin` (§22.7) — clears the flag with a **mark-reviewed** action (§17), the same authority that graduates an entry. Clearing stamps `reviewed_at`/`reviewed_by` for provenance, paralleling `graduated_at`/`graduated_by`. The flag is a property of the entry (frontmatter, §2 amendment), so it is git-truth and survives a cache rebuild, exactly like `state`. The §7 catalog gains an **unreviewed filter** so an owner can find the entries awaiting review; it is the natural worklist for the mark-reviewed action. `unreviewed` only applies to `active` entries — a `super-draft` is pre-review by definition, and `withdrawn` is out of scope. ### 22.5 Project visibility Each project carries a `visibility`, defaulting to **gated**: - **`gated`** (default) — the project is invisible to non-members. It does not appear in the directory (§22.10), its RFCs return 404 to non-members, and reading or writing anything in it requires membership (§22.6). This is the baseline because a deployment may host specs and vision docs it is not ready to publish. - **`public`** — any visitor may read the project's RFCs under the §6.1 anonymous-read contract; the project appears in the directory; contributing still requires a `project_contributor` grant. This is the mode that preserves the pre-multi-project open-by-default behavior, and the mode a generated default project (§22.13) is seeded into. - **`unlisted`** — readable by anyone with a direct link, but not shown in the directory and not enumerated by `GET /api/deployment`. Visibility is the project's; it does not relax the §11 per-branch `read_public` controls *within* a project, which continue to apply on top. ### 22.6 Project membership and roles Membership is a `project_members(project_id, user_id, role, granted_by, granted_at)` table, one role per (user, project). The role is a **new middle tier** between the §6.1 deployment roles and the §6.3 per-RFC authority: 1. **`project_viewer`** — read the project's RFCs and participate in discussion (chat, flags) on anything readable. No propose/branch/PR. The discuss-only counterpart of the §12 per-RFC `discussant`, at project scope. 2. **`project_contributor`** — everything a viewer can do, plus the §6.1 contributor capabilities *within this project*: propose RFCs into it, create branches, open PRs, claim unclaimed super-drafts. 3. **`project_admin`** — everything a contributor can do, plus the §6.1 admin capabilities *within this project*: manage its membership, act on any RFC in it (merge on behalf of arbiters, graduate, set branch visibility, withdraw/reopen), and edit per-RFC delegated authority. `project_admin` is the §6.3 delegated-authority idea lifted from per-RFC to per-project: an admin scoped to one corpus, not the deployment. Membership and role are still gated by the deployment-level `users.permission_state='granted'` (§6): a pending account has no write capability in any project regardless of its `project_members` rows. ### 22.7 How the three tiers compose Authorization for an action on an RFC resolves by taking the **most permissive** of: - the actor's **deployment role** (§6.1) — `owner`/`admin` are superusers in every project; a plain authenticated `contributor` has, by itself, only anonymous-equivalent access to a project until §22.6 grants it a role (subject to §22.5 visibility); - the actor's **project role** in that RFC's project (§22.6); - the actor's **per-RFC authority** in that RFC (§6.3 `owners`/`arbiters`, §12 `rfc_collaborators`). Concretely: deployment `owner`/`admin` ⊇ `project_admin` ⊇ RFC `owners`/`arbiters`; deployment `contributor` + `project_contributor` ⊇ RFC `rfc_collaborators(contributor)`; `project_viewer` ⊇ `discussant`. The §6.2 write-mute and the §22.5 visibility gate are subtractive on top of whatever the union grants. `users.role` (§5) now means *deployment* level only. No schema change demotes an existing owner/admin; their powers simply read as "superuser in every project" rather than "superuser in the corpus." ### 22.8 Discovery and joining a gated project Because a gated project is invisible to non-members, joining is by one of: - **Invite** — a `project_admin` (or deployment admin/owner) adds a user directly, writing a `project_members` row and fanning a §15 notification. This reuses the §12 per-RFC invitation machinery, re-scoped to the project. - **Request to join** — a surface analogous to §28's contribution-requests: a user who knows a project exists (e.g. by direct link to an `unlisted` project, or by out-of-band referral) can request membership; a `project_admin` accepts or declines from the inbox. The request names the desired role (defaulting to `project_viewer`). A `public` project needs neither: read is open, and the existing §6 / §12 contribute-grant paths cover write access. ### 22.9 Branding is resolved at runtime `VITE_APP_NAME` is **deprecated** (§20 amendment): a single build-time name cannot serve N projects. Deployment and project identity are served at runtime: - `GET /api/deployment` — the deployment `name`, `tagline`, and the list of projects the caller can see (gated projects filtered by the caller's membership; `unlisted` omitted). - `GET /api/projects/:id` — that project's `name`, `tagline`, philosophy pointer, and optional `theme` token overrides applied over the §-default `tokens.css`. The frontend reads these instead of `import.meta.env.VITE_APP_NAME`. Two chrome layers result: **deployment chrome** (the directory/landing, the project switcher, the shared inbox) and **project chrome** (the §7 catalog, the §8 RFC view, the §14 philosophy — all per project). ### 22.10 Routing and the deployment landing Every corpus-scoped route gains a project segment: `/p//…` carries the §7 catalog, the §8 entry view at `/p//e/`, the §9/§10 `/p//proposals/`, and the §14 `/p//philosophy`. The entry segment is the **generic `/e/`** for every type — the displayed noun ("RFC", "Spec", "Feature") is a type-driven label (§22.4a), not part of the path, so routing stays a single type-agnostic path and avoids colliding with the reserved sibling segments (`proposals`, `philosophy`, …). The root `/` is the **deployment landing**: a directory of the projects the visitor can see (per §22.5), plus sign-in. An anonymous or non-member visitor sees only `public` projects there. The §8.1 breadcrumb gains a leading project segment: `OHM / Human › main` (slug, with the type-driven noun as its label). ### 22.11 Notifications span projects, one inbox Accounts are deployment-wide, so the §15 inbox is one inbox across all the caller's projects. `notifications` and `watches` carry `project_id` (§5 amendment) so the inbox filters by project and a user can mute an entire project. Quiet hours, digest cadence, and email preferences stay per-account at the deployment level (§5, §15). ### 22.12 Per-project model universe A project's `enabled_models` (registry, §22.2) defines its operator universe, overriding the deployment `ENABLED_MODELS` (§18) when present and falling back to it when absent. The §6.6 per-RFC `models:` list and the §6.7 funder universe resolve *within* the project's universe — the resolution order becomes funder universe ∩ §6.6 list ∩ project universe, with the project universe substituting for the deployment universe at the outermost step. ### 22.13 Migration — the default project (the N=1 case) A deployment upgrading from a pre-multi-project version is migrated to a single **default project** so it keeps running unchanged: 1. The migration generates a `projects` row from current config: `META_REPO → content_repo`, `VITE_APP_NAME → name`, `visibility = public` (preserving the deployment's current open-by-default posture), `type = document` (every pre-multi-project corpus is a document corpus), and the `id` a **config-derived slug**: `DEFAULT_PROJECT_ID` if set, else a slug of the deployment name, falling back to the literal `default`. (M1's migration 026 seeds the bootstrap id `default`; the §C-M3 step re-stamps it to the config-derived slug before any `/p//` route is public, so the id is meaningful — e.g. `/p/ohm/…` — and never renamed after URLs go live. The id stays framework-generic: the framework supplies no deployment name.) 2. Every existing RFC-scoped row (§5 amendment list) is stamped with that `project_id`. 3. Old corpus-root URLs (`/rfc/`, `/proposals/`) 308-redirect to their `/p//…` equivalents — the entry view to `/p//e/` (§22.10) — so existing links survive. 4. The operator creates the registry repo (§22.2) declaring the default project; until they add a second project, the deployment is functionally identical to before, with one extra path segment. This is the §20.4 upgrade-steps content for the release. --- # Part B — Amendments to existing sections Applied in place, in the established amendment-note style (cf. §1's "Topology change (v0.31.0)"). - **§1 Repository topology.** Add the §22 amendment note (above). "This single repository is its content repository" → "each *project* names one content repository; the deployment's registry (§22.2) lists them." The bot and app-owned-authorization paragraphs are unchanged and now read org-wide. - **§2 Meta schema / §2.3 IDs.** Slugs are unique within a project; entry filenames are unchanged (per content repo). §2.3's `RFC-NNNN` `max+1` allocation is **removed** — the slug is the identity (§22.4); there is no per-project number. Entries graduated before this change keep their `id` as a frozen, read-only legacy display label (§22.4), never used for lookup. The entry frontmatter schema becomes type-dependent (§22.4a): `document` keeps today's fields, `specification` and `bdd` add their type metadata. New `active`-entry fields: `unreviewed` (bool) and the `reviewed_at`/`reviewed_by` provenance pair (§22.4c), paralleling `graduated_at`/`graduated_by`. - **§2.4 State machine.** The `(no entry) ─[idea-PR merged]→` transition now targets the project's `initial_state` (§22.4b): `super-draft` as today, or straight to `active` when the project (e.g. a `bdd` project) lands entries there — in which case the entry is stamped `unreviewed` (§22.4c). A new `active ─[mark-reviewed, owner/admin]→ active` self-transition clears the flag. The rest of the machine is unchanged. - **§5 Data model.** Add `project_id` to: `branch_visibility`, `branch_contribute_grants`, `stars`, `threads`, `changes`, `watches`, `notifications`, `rfc_invitations`, `rfc_collaborators`, `contribution_requests`, `funder_consents`, the `*_seen` cursors, `actions`, and the §4 cache tables `cached_rfcs` (PK → `(project_id, slug)`; also mirrors the `unreviewed` frontmatter flag, §22.4c, so the §7 catalog filter can query it without reading every entry file), `cached_branches`, `cached_prs`, `pr_resolution_branches`, `proposed_use_cases`. Add the new tables `projects` (carrying the immutable `type`, §22.4a) and `project_members` (§22.2, §22.6). `users.role` is annotated as deployment-scope (§22.7). - **§6.1 Roles.** Add the §22.7 composition note: deployment roles are now one of three tiers; a plain `contributor` has no implicit access to a project until §22.6 grants a project role (subject to §22.5). - **§6.3 Per-RFC delegated authority.** Note that `project_admin` (§22.6) is the same delegation idea at project scope, sitting above per-RFC authority. - **§7 Left pane.** The catalog is per-project, under `/p//`. The deployment directory (§22.10) is a new surface above it. The catalog gains an **unreviewed filter** (§22.4c) — the owner's worklist of `active` entries that landed unreviewed. - **§8.1 Breadcrumb.** Gains a leading project segment (§22.10). The entry is named by its slug, not a number (§22.4); the noun shown around it ("RFC", "Spec", "Feature") is the project type's label (§22.4a). - **§13.3 Graduation flip.** Operates on the project's content repo. It flips status and moves the entry, but **allocates no number** — the slug is the identity throughout (§22.4); the old per-project `RFC-NNNN` allocation is gone. Graduation is also **conditional on the project's `initial_state` (§22.4b)**: a project that lands entries `active` has no super-draft phase, so the graduate action is a no-op there and is not surfaced. For those entries the **mark-reviewed** action (§22.4c) takes graduation's place as the owner/admin vetting step — it clears `unreviewed` instead of flipping state. - **§14.1 Pre-login landing.** Splits into deployment landing (the directory, §22.10) and per-project philosophy/deck (§14 under `/p//`). Deployment name comes from `GET /api/deployment`, not `VITE_APP_NAME`. - **§17 Backend surface.** RFC routes gain the `/p/` / `project_id` scoping; add `GET /api/deployment`, `GET /api/projects/:id` (returns the project's `type`, §22.4a), and the `project_members` management + request-to-join endpoints (§22.6, §22.8). Add a **mark-reviewed** endpoint clearing an entry's `unreviewed` flag (§22.4c, owner/admin), and an `unreviewed` filter param on the catalog list. Type- specific surfaces (§22.4a) add their own routes, mounted only for projects of the matching type — e.g. the `specification` release-planning endpoints and the `bdd` scenario/coverage endpoints. - **§18 Stack.** `ENABLED_MODELS` is the deployment fallback; per-project `enabled_models` overrides it (§22.12). - **§20 Versioning / surface.** `VITE_APP_NAME` deprecated in favor of the registry + `GET /api/deployment` (§22.9). New required backend env var `REGISTRY_REPO` (the §20.3 env contract); `META_REPO` becomes legacy, consulted only by the §22.13 migration to seed the default project's `content_repo`, then unused. The registry file shape and the `projects`/`project_members` schema join the §20.3 versioned surface. The release is a pre-1.0 minor with a §22.13 upgrade-steps block. Note for the changelog: a deployment assembled by an external operator tool upgrades through the same pinned-version path as any other — the only deploy-surface change is swapping the `META_REPO` overlay value for `REGISTRY_REPO`; the framework's versioned contracts (`/api/health`, `VERSION`, the pin file) are unchanged. --- # Part C — Slicing plan Seven slices carry §22 and its amendments end-to-end. The ordering mirrors DEV.md's original principle — foundations and the cache/permission spine first, the surfaces that consume them after, hardening last. Each slice is shippable: a deployment can stop at any slice boundary and still run (the default project keeps the N=1 case whole throughout). The project `type` (§22.4a) rides M3 (config) and M5 (its surfaces); M1–M4 are type-agnostic because the engine is. **M1 — The project spine (schema + default-project migration).** *(landed)* The `projects` and `project_members` tables; `project_id` threaded additively onto every slug-bearing §5 table (migration 026); the §22.13 default project generated and every existing row backfilled to it; the startup backfill that fills the default project's `content_repo` from `META_REPO`; `REGISTRY_REPO` wired into config (consumed in M3). No UI, no routing change, no registry mirror yet — the app runs exactly as before, single project, with the spine underneath. Additive only: no table rebuilds (the slug-keyed uniqueness/PK rework is deferred to the slice that activates project #2, enumerated in migration 026's header). This is the foundation everything after builds on. **M2 — Project-scoped authorization + the §22.7 resolver.** *(landed)* The three-tier composition: `project_members` roles, the most-permissive union with deployment role and per-RFC authority, the §22.5 visibility gate as a 404 on read and 401/403 on write. Every §17 write endpoint surveyed in §6.1's audit re-checked under the project axis. Still single visible project; verifiable by granting/revoking roles on the default project and flipping its visibility (`backend/tests/test_multi_project_authz_vertical.py`). Pure app-layer — the resolver primitives live in `app/auth.py` (`project_visibility`, `project_member_role`, `is_project_superuser`, `can_read_project`, `can_contribute_in_project`, `require_project_readable`, `visible_project_ids`), composed into the existing per-RFC capability helpers and threaded into every RFC-resolution gate (`_require_rfc*`, `_require_super_draft`, `_require_rfc_readable`, the branch/PR/graduation deep gates). No migration (M1 shipped the tables) and no behavior change on the public default project. Two operator decisions pin how the implicit grant behaves on a `public` project: **implicit-on-public** — a granted deployment `contributor` keeps its pre-multi-project write baseline with no `project_members` row, so the N=1 case stays whole (no backfill); and **preserve curation** — that implicit baseline does *not* override per-RFC owner curation (only an explicit project_contributor/admin or a deployment owner/admin does), so the v0.16.0 per-RFC invite contract is unchanged on public. Explicit `project_members` rows and `gated`/`unlisted` visibility are where the new tier bites. This is a deliberate liberalization of §22.5's literal "contributing still requires a grant" for the public case; gated/unlisted honor the grant model exactly. **M3 — Registry mirror + routing + runtime branding.** The §4 registry mirror (webhook + reconciler over the `REGISTRY_REPO`, populating `projects` rows beyond the default); the **re-stamp of the default project's bootstrap `id`** (`default` → the config-derived slug, §22.13 step 1) which must land here, before any `/p//` URL is public; the `/p//` route prefix with the generic `/e/` entry segment (§22.10) and the 308 redirects off the old corpus-root URLs (`/rfc/` → `/p//e/`); `GET /api/deployment` and `GET /api/projects/:id`; the frontend cut from `VITE_APP_NAME` to runtime config, per-project `theme` token overlay. The deployment directory at `/` and the project switcher in deployment chrome. This slice also adds the additive `type` and `initial_state` columns to `projects` (a small migration — M1 shipped `projects` without them), mirrors both from the registry, returns them on `GET /api/projects/:id`, and drives the entry-noun terminology off `type` (§22.4a). It also teaches the shared creation path to honor `initial_state` (§22.4b) — land a new entry `active` instead of `super-draft`, stamping `unreviewed` and skipping the graduate gate when the project says so — plus the `unreviewed` frontmatter fields (§22.4c) mirrored into `cached_rfcs`, the owner/admin mark-reviewed action, and the §7 catalog unreviewed filter that queries that cached column. But no type *surfaces* yet; beyond their label, landing state, and review flag, all three types still look the same here. After M3 a deployment with two registry projects is fully navigable — which makes this the slice that must also land the deferred slug-keyed uniqueness/PK rebuilds (migration 026 header) before a second project can collide with the first. **M4 — Per-project corpus surfaces (the second-project acceptance pass).** The §7 catalog, §8 entry view, §9/§10 proposal/PR flows, §13 graduation, and §14 philosophy all confirmed working under project scope with the per-project, slug-only namespace (§22.4). This is mostly *inherited* from M1–M3 — the work is an end-to-end pass that proves a second `document` project's full lifecycle (propose → super-draft → graduate, identified by slug *in that project*), not new build. Naming it explicitly as the acceptance slice keeps scope that belongs in M3 from leaking in. **M5 — Type modules + type-specific surfaces.** The per-type layer of §22.4a: type-specific entry-frontmatter validation (`specification`/`bdd` metadata); the `specification` **release-planning** surface; the `bdd` scenario/coverage surface. Type-scoped routes mounted only for matching projects (§17). The shared engine is untouched — this slice only adds the layers on top, so a `document` project is unaffected and the M4 acceptance still holds. (The per-type schema/surface details are the §22.4a draft note's open work.) **M6 — Membership lifecycle.** §22.8 invite (re-scoped §12 machinery) and request-to-join (re-scoped §28); the inbox surfacing of join requests; the §22.11 cross-project inbox with `project_id` filtering and project-level mute. The admin surface for managing a project's roster. **M7 — Hardening + operator path.** Per-project `enabled_models` resolution (§22.12) including funder/§6.6 intersection; the registry's place in `docs/DEPLOYMENTS.md` and the flotilla operator tooling; end-to-end tests spanning two projects with disjoint membership; the §20.4 changelog + upgrade-steps block; the SPEC merge (Part A appended, Part B applied). ## Open items folded into the slices - **Registry repo vs. file-in-existing-repo** — *resolved:* a dedicated registry repo the framework reads via the `REGISTRY_REPO` env var (§22.2). Confirmed against the flotilla operator-tooling spec: the registry is deployment-side git content (like the corpus), not operator config, so the operator tool's only change is swapping the `META_REPO` overlay value for `REGISTRY_REPO`. No flotilla architectural change; no new framework⇄tool contract. With OHM becoming one project among several, the registry sits *above* any single project's content repo, so a file inside one project's repo is wrong — a dedicated repo is the right home. - **Request-to-join vs. invite-only** — drafted with both (§22.8); M6 (membership lifecycle) may ship invite-only first and add request-to-join second if scope demands. - **Per-type schemas and surfaces** — the §22.4a draft note's open work: the `specification` release-planning data model and whether `bdd` scenarios are free-form markdown or a parsed Given/When/Then structure. Pinned in M5.