# 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.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. 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 `name`, `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: `draft`). - **§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 `