diff --git a/docs/design/multi-project-spec.md b/docs/design/multi-project-spec.md new file mode 100644 index 0000000..230fbc7 --- /dev/null +++ b/docs/design/multi-project-spec.md @@ -0,0 +1,371 @@ +# 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 RFC entries under `rfcs/`, with +its own slug and `RFC-NNNN` namespaces, 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 + content_repo: ohm-content # repo under the deployment's Gitea org (§22.3) + visibility: gated # gated | public | unlisted (§22.5) + 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 Slug and ID namespaces are per-project + +An RFC's slug (§2) is unique **within its project**, not across the +deployment: `ohm` and `specs` may each have an `intro`. `RFC-NNNN` integer +IDs are assigned per project at graduation as `max(existing integer IDs in +this project) + 1` (§13, §2.3 amended) — each project numbers from `RFC-0001` +independently. A fully qualified RFC reference is therefore +`(project_id, slug)`, and after graduation `(project_id, RFC-NNNN)`. + +### 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 `/p//rfc/`, the §9/§10 +`/p//proposals/`, and the §14 `/p//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 / RFC-0042 · Human › main`. + +### 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), `id` a + slug derived from the deployment. +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, 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 `max+1` is scoped to the + project (§22.4). +- **§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)`, `rfc_id` + unique per project), `cached_branches`, `cached_prs`, + `pr_resolution_branches`, `proposed_use_cases`. Add the new tables + `projects` 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. +- **§8.1 Breadcrumb.** Gains a leading project segment (§22.10). +- **§13.3 Graduation flip.** Operates on the project's content repo; + `RFC-NNNN` is allocated per project (§22.4). +- **§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`, + and the `project_members` management + request-to-join endpoints (§22.6, + §22.8). +- **§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 + +Six 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). + +**M1 — The project spine (schema + registry + default-project migration).** +The `projects` and `project_members` tables; the §4 registry mirror (webhook ++ reconciler over the registry repo); `project_id` threaded through every §5 +table per the Part B list; the §22.13 migration that generates the default +project, backfills `project_id`, and stands up the redirect. No UI yet — the +app runs exactly as before, single project, with the spine underneath. This +is the slice that must land atomically; everything after is additive. + +**M2 — Project-scoped authorization + the §22.7 resolver.** 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; this slice +is verifiable by granting/revoking roles on the default project and asserting +the gates. + +**M3 — Routing + runtime branding.** The `/p//` route prefix, the +308 redirects, `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. After M3 a deployment with two registry projects is fully +navigable. + +**M4 — Per-project corpus surfaces.** The §7 catalog, §8 RFC view, §9/§10 +proposal/PR flows, §13 graduation, and §14 philosophy all confirmed working +under project scope with per-project slug/ID namespaces (§22.4). Mostly +inherited from M1–M3; this slice is the end-to-end pass that proves a second +project's full lifecycle (propose → super-draft → graduate → `RFC-0001` *in +that project*). + +**M5 — 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. + +**M6 — 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); M5 may + ship invite-only first and add request-to-join second if scope demands. diff --git a/docs/design/multi-project.md b/docs/design/multi-project.md new file mode 100644 index 0000000..3e622bb --- /dev/null +++ b/docs/design/multi-project.md @@ -0,0 +1,255 @@ +# Design sketch — multi-project deployments + +> Status: **draft / sketch.** Not binding. This precedes the SPEC edits it +> describes. Decisions captured here were made interactively; open questions +> are flagged inline. When this stabilizes it folds into `SPEC.md` (§1, §2, +> §5, §6, §7, §8, §13, §14, §17, §20) and ships as a pre-1.0 minor with +> upgrade steps. + +## The reframe + +Today the framework hardcodes **deployment : corpus = 1 : 1**. One deployment +is one Gitea content repo (`META_REPO`), one global slug namespace, one +`VITE_APP_NAME` baked into the build, one flat catalog. SPEC §1 says it +plainly: "this single repository is its content repository." + +This change makes it **deployment : project = 1 : N**, where *today's entire +deployment becomes the N=1 case*. A **project** is what a corpus is now: a +content repo, its own slug/`RFC-NNNN` namespace, its own catalog, philosophy, +branding, member roster, and enabled-models universe. The **deployment** (the +subdomain — e.g. Wiggleverse) becomes a thin shell hosting a directory of +projects plus a shared account/notification layer. + +OHM becomes one project among several (specs, vision docs, …) under one +deployment. + +The value of this framing: the migration stays mechanical. Everything +deployment-scoped today splits cleanly into: + +- **stays deployment-scoped** — accounts, the beta/permission gate, the inbox, + the bot service account, the Gitea org; +- **becomes project-scoped** — the corpus, branding, roles, catalog, philosophy, + enabled models. + +## Decisions (locked) + +1. **Project registry lives in git.** A registry (a `projects.yaml` / registry + repo) declares which projects exist and their config; adding a project is a + PR. Mirrored into a `projects` cache table. Keeps the git-is-truth invariant. +2. **Projects are membership-gated by default** (private). A non-member does + not see a private project exists. `visibility` is still a per-project field + with `public` and `unlisted` escape hatches (see §3) — gated is the default, + not the only mode. +3. **RFC numbering is per-project.** Each project has its own `RFC-0001`, + `RFC-0002`… computed as `max(existing IDs in this project) + 1`. + +## 1. Git topology — one content repo per project + +One Gitea org for the deployment, **N content repos** (`ohm-content`, +`specs-content`, `vision-content`, …). The bot already operates org-wide; it +gains more repos and one registry repo. Slug uniqueness and `RFC-NNNN` +sequences become naturally per-repo = per-project. + +Rejected alternatives: subdirectories in one repo (`projects//rfcs/…`) +churns every path / branch-name / graduation code path and can't carry +git-layer access control per project; prefixed slugs pollute the namespace. + +### The registry repo + +A small repo (or a top-level file in a deployment repo) the bot reads and +mirrors. Sketch shape: + +```yaml +# projects.yaml +deployment: + name: Wiggleverse + tagline: ... +projects: + - id: ohm # url slug, stable, unique in deployment + name: Open Human Model + content_repo: ohm-content # repo under the deployment's Gitea org + visibility: gated # gated | public | unlisted + philosophy_repo_path: PHILOSOPHY.md + enabled_models: [claude, gemini] # optional; falls back to deployment ENABLED_MODELS + theme: { accent: "#5b5bd6" } # optional per-project token overrides +``` + +Project **definition/config** is in git; project **membership** is in the DB +(it churns like `rfc_collaborators` and is app-state, not document state). The +registry gets the same webhook + reconciler treatment as content repos. + +> Open: does the registry get its own repo, or is it a file in an existing +> deployment/meta repo? Ties into the `ohm-rfc-app-flotilla` operator tooling, +> which already owns deploy orchestration and could own registry edits. + +## 2. Data model + +Introduce a **`projects`** cache table (mirrored from the registry, like +`cached_rfcs` is mirrored from content). Then thread `project_id` through +every RFC-scoped table. + +Hard constraint: once slugs are unique only *within* a project, any table +keyed on `rfc_slug` alone is ambiguous, so `project_id` must ride along on all +of them: + +- `cached_rfcs` — PK becomes `(project_id, slug)`; `rfc_id` unique per project +- `cached_branches`, `cached_prs`, `pr_resolution_branches`, `proposed_use_cases` +- `threads`, `changes`, `branch_visibility`, `branch_contribute_grants` +- `stars`, `watches`, `pr_seen`, `branch_chat_seen` +- `rfc_invitations`, `rfc_collaborators`, `contribution_requests`, `funder_consents` +- `notifications`, `actions` + +~15-table migration, all backfillable to a single default project (see §7). +`api_graduation.py`'s `RFC-NNNN` allocator scopes its `max()` query by +`project_id`. + +New tables: + +``` +projects( + id TEXT PRIMARY KEY, -- 'ohm' + name TEXT NOT NULL, + content_repo TEXT NOT NULL, + visibility TEXT CHECK (visibility IN ('gated','public','unlisted')), + config_json TEXT, -- theme, tagline, enabled_models, … + registry_sha TEXT, -- provenance of the mirrored row + updated_at TEXT +) + +project_members( + project_id TEXT NOT NULL REFERENCES projects(id), + user_id INTEGER NOT NULL REFERENCES users(id), + role TEXT CHECK (role IN ('project_admin','project_contributor','project_viewer')), + granted_by INTEGER REFERENCES users(id), + granted_at TEXT, + PRIMARY KEY (project_id, user_id) +) +``` + +## 3. Roles — three tiers + +A **middle tier** slots between today's deployment roles and per-RFC authority. + +| Tier | Who | Powers | +|---|---|---| +| **Deployment** (unchanged, narrowed) | `owner` / `admin` | Create/archive projects, manage all accounts, act in any project. The beta/`permission_state` gate stays here — it gates *having an account*, not project access. A plain authenticated user has an account but no implicit project powers. | +| **Project** (NEW — `project_members`) | `project_admin` / `project_contributor` / `project_viewer` | `project_admin` = today's app-admin, scoped to one project (manage its membership, settings, graduate, act on any RFC in it). `project_contributor` = propose/branch/PR/chat. `project_viewer` = read + discuss only. | +| **RFC** (unchanged) | frontmatter `owners`/`arbiters`; `rfc_collaborators` (`contributor`/`discussant`) | Same as today, now scoped within their project. | + +This is the existing owner → admin → contributor delegation pattern with a +project axis added. `users.role` reverts to meaning *deployment*-level only. + +**Visibility interaction:** + +- **gated** (default) — invisible to non-members; must be a member to see it + exists. Read and write both require membership. +- **public** — any authenticated user can read; contributing requires a + `project_contributor` grant. +- **unlisted** — readable by direct link, not shown in the directory. + +> Philosophy tension to resolve in SPEC: today the app is open-by-default +> (anonymous read, §11.1; admission gates only writing). Gated-by-default +> reverses that for the common case. The `public`/`unlisted` modes preserve the +> old behavior for projects that want it, and the deployment can choose its own +> default posture — but §11 and §14 need rewriting to make "gated" the baseline +> and anonymous read a per-project opt-in. + +**Discovery for gated projects:** since a stranger sees an empty directory, +there must be a join path — invite-only (a `project_admin` adds you), or a +request-to-join surface analogous to the existing §28 contribution-request +flow. (Open — pick one.) + +## 4. Branding & frontend (forced change) + +The one *forced* change. `VITE_APP_NAME` is baked at build time; you cannot +bake N project names into one bundle. Branding moves to **runtime config +served by the backend**: + +- `GET /api/deployment` → deployment name/tagline + the list of projects the + caller can see (gated ones filtered by membership). +- `GET /api/projects/:id` → that project's name, tagline, philosophy, theme + tokens. +- Frontend reads these instead of `import.meta.env.VITE_APP_NAME` + (`App.jsx:208`, `Landing.jsx:16`, `BetaPending.jsx:17`). + +`VITE_APP_NAME` is deprecated → deployment name comes from the registry. This +is a documented config change with upgrade steps per CLAUDE.md. + +Two chrome layers result: + +- **Deployment chrome** — the Wiggleverse header, the project directory / + landing, a project switcher. +- **Project chrome** — the current header / catalog / philosophy, now per + project; theme tokens (`tokens.css`) overridable per project at runtime. + +## 5. Routing & UX + +- RFC routes gain a project prefix: `/p//rfc/`, + `/p//proposals/`, `/p//philosophy`, etc. +- Root `/` becomes the **deployment landing = project directory** (the + Wiggleverse home). For an anonymous or non-member visitor under gated + default, that's only public/unlisted-by-link projects. +- Breadcrumb gains a segment: `Wiggleverse / OHM / RFC-0042 · Human › main`. +- The left-pane catalog (§7) becomes per-project; a project switcher lives in + deployment chrome. + +## 6. Cross-cutting — notifications, inbox, watches + +Accounts are deployment-wide, so there is **one inbox spanning projects** +(§15). `notifications` and `watches` carry `project_id` so the inbox is +filterable and a user can mute an entire project. Quiet hours / digest prefs +stay per-account (deployment level). + +## 7. Backward compatibility & migration + +The N=1 path keeps existing single-project deployments working: + +1. Migration creates one **default project** from current config (`META_REPO` + → `content_repo`, `VITE_APP_NAME` → `name`, visibility seeded to match the + deployment's current open posture, likely `public`). +2. Every existing row's `project_id` is stamped to that default project. +3. An optional default-project redirect keeps old `/rfc/` URLs alive + (308 → `/p//rfc/`). + +This is the SPEC §20 upgrade-steps block. + +## 8. SPEC & versioning impact + +Touches §1, §2, §5, §6, §7, §8, §13, §14, §17, §20. Pre-1.0 minor with +breaking changes spelled out (schema migration, `VITE_APP_NAME` deprecation, +URL change, gated-default philosophy shift). Single-process SQLite stays fine — +`project_id` is just a column; no DB-per-project, no Postgres forced. + +## Operator-tooling integration (flotilla) + +Checked against the `ohm-rfc-app-flotilla` spec (the OHM deployment's operator +control panel). It assembles a deployment from `{rfc-app@pin} + {non-secret +overlay} + {secret pulls} + {corpus}`, **does not host the corpus** ("the +corpus lives in a deployment-side repo"), and **depends on the framework only +through versioned contracts** (`/api/health`, `VERSION`, CHANGELOG +upgrade-steps, the `.rfc-app-version` pin). The framework knows nothing about +flotilla. + +This confirms the registry decision and fixes how it integrates: + +- The registry is **deployment-side git content the framework reads** — same + category as the corpus — *not* a flotilla-owned config blob. Putting + project definitions in operator tooling would re-bake deployment specifics + into the assembly layer and add a new framework⇄tool contract. +- The framework reads the registry via a `REGISTRY_REPO` env var, the + multi-project successor to `META_REPO`. Flotilla's overlay simply swaps one + ref; its four versioned contracts are untouched. +- Multi-project therefore ships to OHM as an **ordinary pinned-version + upgrade**: bump the pin, run the §22.13 default-project migration, change + `META_REPO`→`REGISTRY_REPO` in the overlay, deploy, verify via + `/api/health`. No flotilla architectural change. + +## Open questions + +- Gated discovery: invite-only vs. request-to-join surface? (drafted with + both; M5 can ship invite-only first.) +- Does the deployment landing itself need branding config, or is it derived + entirely from the registry `deployment:` block? +- Per-project `ENABLED_MODELS` resolution vs. deployment universe (§18, §6.6, + §6.7 funder) — confirm fallback order. +- Slicing plan for the build (mirrors DEV.md's original slice approach).