# Changelog The binding policy for what each kind of version bump means and what the changelog has to carry is in [`SPEC.md` §20](./SPEC.md). The practical recipe downstream deployments follow when reading this file is in [`docs/DEPLOYMENTS.md`](./docs/DEPLOYMENTS.md). The canonical version is the `VERSION` file at the repo root; `frontend/package.json#version` mirrors it. Deployments pin to a specific framework version in their own `.rfc-app-version` file (per §20.5). While the framework is pre-1.0, minor-version bumps may introduce breaking changes; the entry below each such bump documents the upgrade steps a deployment must apply. Upgrade-steps blocks use the [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) / [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174) normative- language convention per `SPEC.md` §20.4. **MUST** / **SHALL** steps are required; **SHOULD** steps are recommended (the framework's default and tested path); **MAY** steps are optional. Upgrades that skip versions are the composition of each intervening adjacent release's steps in order — no A-to-B path is pre-computed beyond that. ## 0.38.0 — 2026-06-04 **Minor — §22 M3-backend Plan B (write path, propose): a new entry can be proposed *into a specific project*, landing in that project's content repo and surfacing under that project's proposals. A non-default project is no longer read-only. No upgrade steps; single-project deployments are unaffected.** Added: - **`POST /api/projects/{pid}/rfcs/propose`** — propose into a chosen project (read-gated, then project-level contribute-gated, §22.6/§22.7). The propose body is now a project-parameterized helper; the unscoped `/api/rfcs/propose` stays as the default-project compat path. Slug uniqueness, the idea-PR reservation, the landing state (§22.4b), and the `proposed_use_cases` row are all scoped to the target project. - **`GET /api/projects/{pid}/proposals`** — pending idea-PRs scoped to one project. - **Per-project PR mirror** (`app/cache.py`): `refresh_meta_pulls` iterates every project's `content_repo`, stamping `cached_prs.project_id` (was: the default project only). `projects.content_repo(pid)` helper added. - **Tests:** `test_project_scoped_propose.py` — propose into a second project lands in its content repo + shows only under its proposals (not the default's); gated-project propose 404s a non-member. 447 backend green. Changed: - **Frontend:** `api.proposeRFC(projectId, …)` / `listProposals(projectId)`; `ProposeModal` takes a `projectId`; `App` resolves the current project from the `/p//` URL so the propose modal targets it; `Catalog` lists that project's proposals. Known limitation (next slice): the **edit** write flows — branch / PR / graduation — and the **default-project-id re-stamp** (§22.13 step 1) are not yet project-scoped (they still target the default project's content repo). Per `docs/superpowers/specs/2026-06-04-m3-backend-planb-design.md` §1 + §3. ## 0.37.0 — 2026-06-04 **Minor — §22 M3-backend Plan B (2/2, read path): per-project RFC serving. A second project's corpus now renders under `/p//`, isolated by its own slug namespace. No upgrade steps; single-project deployments are unaffected.** This is the slice that makes "multiple projects" *observable*: M3-frontend (v0.35.0) shipped the shell with a "not served" guard for any non-default project; v0.36.0 rebuilt the keys so project #2 can exist; this serves project #2's corpus. Added: - **Per-project read endpoints** (`app/api.py`): `GET /api/projects/{pid}/rfcs` (catalog scoped to one project) and `GET /api/projects/{pid}/rfcs/{slug}` (entry by `(project_id, slug)`), both behind the §22.5 read gate (gated project 404s a non-member). The unscoped `/api/rfcs[/{slug}]` remain as the default-project compat path. - **Per-project corpus mirror** (`app/cache.py`): `refresh_meta_repo` now iterates every `projects` row and mirrors each project's `content_repo` into `cached_rfcs` stamped with that `project_id` (was: the default project only). - **Tests:** `test_project_scoped_serving.py` — catalog scoping, entry isolation (same slug under two projects resolves distinctly; a slug present only in one 404s under the other), gated-project 404. 445 backend green. Changed: - **Frontend** reads the scoped routes: `api.listRFCs(projectId)` / `getRFC(projectId, slug)`; `Catalog` + `RFCView` pass `useProjectId()`. The **M3-frontend `NotServedPlaceholder` guard is removed** — every registry project renders its corpus. `ProjectLayout` keeps the 404/not-readable branch. Known limitation (next slice): the **write** path (propose / branch / PR / graduate) and the **default-project-id re-stamp** (§22.13 step 1) are not yet project-scoped — write affordances still target the default project, so a non-default project is effectively read-only until Plan B's write slice. (No live impact: no deployment runs a second project yet.) Per `docs/superpowers/specs/2026-06-04-m3-backend-planb-design.md` §1 + §3 (write). ## 0.36.0 — 2026-06-04 **Minor — §22 M3-backend Plan B (1/2): the slug-keyed PK/UNIQUE rebuild that activates project #2. No behavior change (deployments are still single-project `default`); migration `028` runs automatically on deploy.** This lands the table rebuilds migration 026 deliberately deferred "until a second project exists" — folding `project_id` into the slug-keyed PRIMARY KEY / UNIQUE constraints so two projects can hold the same slug without colliding. Shipped separately from per-project RFC *serving* (Plan B 2/2) for migration hygiene: the schema change deploys and is verified on its own, smaller blast radius. Added: - **Migration `028_project_scoped_keys.sql`** — rebuilds 13 tables to composite slug keys: `cached_rfcs` PK `(slug)` → `(project_id, slug)`; the `UNIQUE`/PK on `cached_branches`, `branch_visibility`, `branch_contribute_grants`, `stars`, `watches`, `pr_seen`, `branch_chat_seen`, `funder_consents`, `rfc_collaborators`, `contribution_requests`, `proposed_use_cases` all gain `project_id`; and the FKs **to** `cached_rfcs(slug)` on `rfc_invitations`, `rfc_collaborators`, `contribution_requests` become composite `(project_id, rfc_slug) → cached_rfcs(project_id, slug)`. (`cached_prs` is unchanged — `(repo, pr_number)` is already globally unique.) - **Migration-runner capability** (`db.run_migrations`): a migration whose first line is `-- migrate:no-foreign-keys` runs with `PRAGMA foreign_keys` toggled OFF around it (required by SQLite's table-rebuild procedure, and a no-op inside a transaction) and a `PRAGMA foreign_key_check` after that fails the migration loudly on any dangling reference. Changed: - The `ON CONFLICT(...)` upsert targets for the rebuilt tables gain `project_id` (`cache.py`, `api_prs.py`, `api_branches.py`, `api_notifications.py`, `api.py`, `funder.py`) so they continue to match the new composite indexes. The inserted `project_id` still defaults to `default`, so behavior is identical for a single-project deployment. Upgrade steps: 1. **MUST** deploy. Migration `028` runs automatically at startup; it rewrites the listed tables in one transaction with FK enforcement off and verifies `foreign_key_check` after. No data is dropped (rows already carry `project_id` from migration 026, M1). No config change. 2. **MAY** note: per-project RFC *serving* (path-scoped endpoints + the default-id re-stamp + the frontend guard removal) is the next slice (Plan B 2/2), per `docs/superpowers/specs/2026-06-04-m3-backend-planb-design.md`. ## 0.35.0 — 2026-06-04 **Minor (breaking) — §22 M3 frontend: `/p//` routing, runtime branding (the `VITE_APP_NAME` hard cut), the deployment directory + project switcher, and server-side 308 redirects off the old corpus-root URLs. Completes the runtime-config cut 0.33.0 began.** Added: - **`DeploymentProvider`** (`src/context/DeploymentProvider.jsx`) — boots `GET /api/deployment` once and provides `{ name, tagline, defaultProjectId, projects[], loading }` to the tree. The neutral `brandTitle()` fallback (`'RFC'`) paints during the pre-fetch frame. - **`/p/:projectId/*` routing** with the generic `/e/` entry segment (§22.10). **`ProjectLayout`** (`src/components/ProjectLayout.jsx`) fetches `GET /api/projects/:id`, applies the project's `theme` as `:root` CSS custom-property overrides (reset on switch/unmount so accents never bleed), provides `ProjectContext`, and sets the tab title to the project name. - **The §4 guard** — `ProjectLayout` renders the corpus only for the corpus-served (default) project; any other id renders a "content not yet served" placeholder (`NotServedPlaceholder`), so per-project serving (the next backend slice, Plan B) can land without a wrong-content footgun. - **Deployment directory** at `/` (`Directory.jsx`) — renders the caller-visible projects as cards when **2+** are visible; the **N=1** case redirects straight into the single project (`/p//`), preserving OHM's "land in the corpus" UX. A **project switcher** (`ProjectSwitcher.jsx`) rides deployment chrome when 2+ projects are visible. - **Entry-noun terminology** (RFC / Spec / Feature) driven by `project.type` (§22.4a); the route segment stays the generic `/e/`. - **`GET /api/deployment`** now returns **`default_project_id`** — the corpus-served project the frontend guard keys on. - **Server-side 308 redirects** (`app/api_deployment.py`): `GET /rfc/`, `/rfc//pr/`, and `/proposals/` permanently redirect to `/p//e/[…]` / `/p//proposals/` (§5/§22.10), so external "RFC-0001"-style links and bookmarks keep working. Breaking: - **`VITE_APP_NAME` is removed.** The build no longer reads it and no longer fails without it; the deployment name comes from the registry (`deployment.name` in `projects.yaml`) served at runtime via `GET /api/deployment`. The same build now serves any deployment. The build-time `%VITE_APP_NAME%` HTML token and the `inject-app-name` Vite plugin are gone; `index.html` ships a static `RFC` and JS sets the real title after config loads. - **Entry/proposal URLs moved under `/p//`.** The old SPA routes `/rfc/:slug`, `/rfc/:slug/pr/:n`, `/proposals/:n` are removed from the SPA and served as backend 308s instead — so nginx must route `/rfc/` and `/proposals/` to the backend rather than the SPA `index.html`. Scope boundary (informational, not breaking): RFC *data* is still served unscoped for the default project this slice — per-project corpus serving is the next backend slice (Plan B). Non-default projects show the placeholder. Upgrade steps: 1. **MUST** update nginx: add `location /rfc/` and `location /proposals/` blocks that `proxy_pass` to the backend, **before** the SPA `location /` fallback. The framework's `deploy/nginx/ohm.wiggleverse.org.conf` (and `testing/web.nginx.conf`) already carry them; a deployment running a custom vhost MUST add them, or old corpus-root URLs 404 against the SPA instead of redirecting. 2. **MUST** ensure the registry `projects.yaml` `deployment.name` is set — it is now the header brand and tab title (already required since 0.33.0). `tagline` shows on the directory. 3. **SHOULD** remove `VITE_APP_NAME` from `frontend/.env`; it is no longer read (no error if left — simply ignored). 4. **MUST** rebuild the frontend and deploy. Verify: the header shows the deployment name from `/api/deployment`; `/` lands in your project (N=1) or shows the directory (2+); an old `/rfc/` URL **308**-redirects to `/p//e/`; `/api/health` is green. 5. **MAY** note: the Tier-1 Playwright e2e for these flows lands once the Tier-1 Docker stack seeds a registry repo + `projects.yaml` (`REGISTRY_REPO` is unset in `testing/.env.tier1` today, so the dockerized backend can't boot there post-0.33.0). This slice is covered by Vitest unit tests (`DeploymentProvider`, `ProjectLayout` theme/guard, `Directory`) + the backend redirect tests (`backend/tests/test_api_deployment.py`). ## 0.34.0 — 2026-06-04 **Minor — containerize rfc-app for per-PR preview environments (flotilla SPEC §15). Additive: production is unaffected — it still deploys via the pin-based on-VM gesture. No upgrade steps for existing deployments.** Adds a `Dockerfile` (+ `deploy/preview/`) so the operator's flotilla can build a PR's tree and run it as an ephemeral, scale-to-zero Cloud Run preview with a seeded **synthetic** database and **zero real secrets** (test-secret env only): - `Dockerfile` — multi-stage: Vite SPA build → Python runtime serving the SPA via nginx on `$PORT` and reverse-proxying `/api/`,`/auth/` to a single-process uvicorn on `127.0.0.1:8000` (mirrors the prod nginx + systemd split, minus TLS, minus prod secrets). Single process, single SQLite file (§4.2). - `deploy/preview/entrypoint.sh` — renders nginx against Cloud Run's `$PORT`, boots uvicorn (which runs migrations), and applies the synthetic seed on a fresh DB. - `deploy/preview/seed.sql` — version-controlled synthetic fixture (no PII). - `deploy/preview/preview.env.example` — the test-secret env shape (Cloudflare always-pass Turnstile keys, a Mailpit SMTP sink, analytics no-op'd) the operator loads into flotilla's preview overlay layer. The framework still knows nothing about flotilla — the image is a generic container of rfc-app; flotilla is one possible orchestrator of it. ## 0.33.0 — 2026-06-04 **Minor (breaking) — §22 M3 backend Plan A: project registry mirror + runtime config. The framework now learns its projects from `REGISTRY_REPO/projects.yaml`; `META_REPO` is retired. Migration `027` runs automatically on deploy.** Added: - **Project registry mirror** (`app/registry.py`): the framework reads `projects.yaml` from `REGISTRY_REPO` and mirrors its `deployment:` block and `projects:` entries into the `projects` table + a new `deployment` singleton. The mirror runs at startup (reconciler) and on every §4 webhook push to the registry repo. - **`GET /api/deployment`** — returns `name`, `tagline`, and the list of visible projects (each item carries `id`, `name`, `type`, `visibility`). Replaces the build-time `VITE_APP_NAME` as the authoritative runtime config source (the frontend cut lands in M3-frontend). - **`GET /api/projects/:id`** — returns `id`, `name`, `tagline`, `type`, `visibility`, `initial_state`, and `theme` for a single project. - **§22.4b `initial_state`** honored at propose time: new RFCs enter the state named by the project's `initial_state` field (default: `super-draft`; `bdd` projects default to `active`). - **§22.4c `unreviewed` flag**: newly proposed RFCs are flagged `unreviewed = true`. Owners clear it via `POST /api/projects/:id/rfcs/:slug/mark-reviewed`. The catalog accepts `?unreviewed=true` to filter to the review queue. - **Migration `027`** (additive): adds `projects.type` / `projects.initial_state`, the `deployment` table, and `cached_rfcs.unreviewed` / `reviewed_at` / `reviewed_by`. Breaking: - `META_REPO` is retired. The app **refuses to start** if `REGISTRY_REPO` is unset. The corpus mirror now reads each project's `content_repo` from `projects.yaml` rather than from the `META_REPO` env var. Upgrade steps: 1. **MUST** create a registry repo under your deployment's Gitea org. 2. **MUST** author `projects.yaml` at its root with a `deployment:` block (`name`, `tagline`) and one `projects:` entry for your existing corpus: ```yaml deployment: name: tagline: projects: - id: default name: type: document content_repo: visibility: public ``` Keep `id: default` for this release; the pretty-slug re-stamp lands in the next backend slice, before any `/p/` URL is public. 3. **MUST** set `REGISTRY_REPO=` and **MUST** remove `META_REPO` (or leave it unset — it is ignored but its presence may cause confusion). 4. **MUST** add a Gitea webhook on the registry repo pointing at `/api/webhooks/gitea` (same secret as the corpus webhook). 5. **MUST** deploy. Migration `027` runs automatically at startup; the registry mirror reconciles immediately after. Verify `GET /api/deployment` returns your project and `/api/health` is green. 6. **SHOULD** rebuild the frontend (no new env var is required until M3-frontend lands, but the frontend currently still reads `VITE_APP_NAME` for the display name). ## 0.32.0 — 2026-06-01 **Minor — graduation's integer RFC number is now optional, and RFC owners + site owners can retire (soft-delete) RFCs. Schema migration `025_retired_state.sql` runs automatically on deploy; a frontend rebuild applies the UI.** Two changes to the §13 lifecycle: 1. **Optional number at graduation (§13.2/§13.3).** Graduation no longer hard-requires a valid `RFC-NNNN`. The Graduate dialog's integer-ID field is now optional: it is still pre-filled with the next free number as a *suggestion*, but the graduating owner may clear it and graduate with **no number**. When blank, the entry flips to `active` with `id: null` and the **slug remains the canonical identifier** (§2.3). `GET …/graduate/check` treats a blank id as valid (`ok:true`); `POST …/graduate` accepts a blank/absent `rfc_id` and only validates the `^RFC-\d{4,}$` regex + collision check when a number *is* supplied. The catalog and RFC view render number-less active entries by their slug/title (no "RFC-undefined"). RFC-0001's existing number is grandfathered — `cached_rfcs.rfc_id` was already nullable, so no data migration was needed for this part. 2. **Retire / un-retire — soft delete (§3, §3.1, §13.7).** A fourth entry state, `retired`, is added. An RFC's own owners (frontmatter) and site `owner`-role holders — **not** app admins — may retire an entry (`POST /api/rfcs//retire`); it flips to `retired` via an auto-merged meta-repo PR, leaving the body and every other field (including any integer id) intact. A retired entry is removed from **every** browsing surface: the catalog, the RFC/discussion/branch views (404), and link pickers/search. It is *not* hard-deleted — the entry stays in `rfcs/` (git is truth). Un-retire (`POST /api/rfcs//unretire`) restores the prior state and is **site-owner-only**, so a soft-delete is always recoverable by the operator but an RFC owner cannot reverse their own retirement. Site owners find retired entries via a new owner-gated admin surface (`GET /api/admin/retired-rfcs`, the "Retired" tab) and un-retire from there or by navigating directly to the entry. Upgrade steps: - **MUST** run the migration. `025_retired_state.sql` rebuilds `cached_rfcs` to widen its `state` CHECK constraint to include `retired` (SQLite cannot alter a CHECK in place). It runs automatically at startup via the forward-only migration runner; existing rows are preserved, and the table is reconstructible from Gitea by the reconciler regardless (§4). No operator action beyond a normal deploy. - **SHOULD** rebuild the frontend so the optional-id Graduate dialog, the Retire affordance, and the owner-only "Retired" admin tab are present. - No config or secret changes. ## 0.31.4 — 2026-06-01 **Patch — bug fix + UI polish: secondary buttons that were invisible on light surfaces now render legibly, and the RFC view's breadcrumb action bar is harmonized into one coherent control group. CSS-only (`frontend/src/App.css`); no schema, API, config, overlay, or secret change — a plain frontend rebuild applies it. No upgrade steps. Shipped from driver session 0059.0.** `.btn-link` was authored as a *dark-header* utility — white text on a translucent-white fill (`rgba(255,255,255,0.15)`), the established on-dark pattern for the app header's "Sign out". But the same class is reused on **light** surfaces: the RFC breadcrumb action bar (`RFCView.jsx`), the PR view's diff-mode toggle and "Edit title" control (`PRView.jsx`), the invitations and inbox modals, and the discussion panel. On those near-white backgrounds the buttons were white-on-white — present in the DOM, fully functional, but visually invisible. The reported symptom: on a super-draft's header, "Metadata", "Claim ownership", and "Invitations" looked *missing*, while the filled CTAs ("Start Contributing", "Graduate to RFC repo") rendered fine because their fill carried them. The fix is root-cause, not a per-site patch: - The base `.btn-link` rule is now a proper light-surface secondary button (white fill, hairline `--c-gray-300` border, `--c-gray-700` label, hover darkens both). This corrects every light-surface reuse at once. - The original translucent-on-dark treatment is preserved for the one legitimate dark-surface use via an `.app-header .btn-link` scope, so the header "Sign out" is unchanged. - The breadcrumb action bar (`.breadcrumb-actions`) normalizes every action — the discuss/contribute toggle, the filled CTAs, and the secondary buttons — to one height, radius, and type scale, so the row reads as a single intentional control group. The bar now `flex-wrap`s instead of clipping buttons off the right edge when the set is wide. - The diff-mode toggle's active option now reads as clearly selected (filled ink) rather than relying on a weight change alone. Smooth hover transitions and the keyboard focus ring were already provided globally by the v0.21.0 interaction-polish layer, so this change adds no new motion or focus rules — it only corrects resting-state color/contrast and harmonizes sizing. ## 0.31.3 — 2026-05-30 **Patch — admin Users tab: "Last seen" reads "Never" for unclaimed invites. Visual/logic only in `Admin.jsx`. A plain frontend rebuild applies it.** An admin-created invite row showed a real-looking "Last seen" timestamp identical to "Signed up," implying the invitee had visited when they hadn't. Cause: `users.last_seen_at` is `NOT NULL DEFAULT (datetime('now'))` (`migrations/001_users_and_audit.sql`) and the invite INSERT (`invites.py`) sets neither timestamp, so both default to the row-creation instant; `last_seen_at` only advances on a real authentication. Since an unclaimed invite has provably never authenticated (that unclaimed state is exactly what drives the "PENDING INVITE" badge), the Users tab now renders **"Never"** for the Last-seen cell of a pending-invite row instead of the misleading default. Signed-up (the invite-created date) is unchanged. Upgrade steps: none. **SHOULD** deploy as a normal code deploy. ## 0.31.2 — 2026-05-29 **Patch — landing (`/`) welcome panel spacing. Visual only: CSS in `App.css` (`.welcome`). A plain frontend rebuild applies it.** The welcome read-view was jammed against the catalog divider with no top offset and loose, uneven paragraph spacing. Root cause: `.main-pane` carries a bare `.main-pane { padding: 0; display: flex }` override (the §8 three-column RFC shell) that shadows the earlier padded read-view rule, so the pane provides no padding — and `.welcome` (just `max-width`) never compensated. The welcome surface now owns its own breathing room: 56px top / 48px side gutters, a capped 680px measure, a stronger `text-3xl` "Welcome." hero, and even `--space-8` paragraph rhythm at `--leading-relaxed`. Applies to both the signed-out and signed-in welcome (same `.welcome` class). Upgrade steps: none. **SHOULD** deploy as a normal code deploy. ## 0.31.1 — 2026-05-29 **Patch — admin Users tab + header UX polish. Visual only: CSS plus markup/structure in `Admin.jsx` (no API, schema, config, overlay, or secret change). A plain frontend rebuild applies it.** Two latent CSS defects fixed: - **`.invite-badge` had no rule.** The "(pending invite)" marker on admin-created-but-unclaimed user rows rendered as bare parenthetical text. It's now a quiet amber pill, consistent with the other status badges. - **`.btn-link-quiet` never reset native button chrome.** Used as a bare link-style `