Release 0.2.0: graduation merge race fix + VITE_APP_NAME + versioning infrastructure

- Fix §13.3 step 4 race against Gitea's async mergeability
  computation: gitea.wait_for_mergeable polls the mergeable
  field before calling merge, and bot._merge_with_retry rides
  out the transient 'Please try again later' response.
- Frontend now requires VITE_APP_NAME at build time
  (frontend/vite.config.js fails the build if unset). Header
  brand, landing H1, and browser title read the deployment's
  configured name.
- Establish framework versioning: VERSION (canonical) +
  frontend/package.json#version mirror; CHANGELOG.md as the
  release log with RFC 2119 / 8174 normative-language
  upgrade-steps; SPEC.md §20 as the binding policy; CLAUDE.md
  capturing the separation-of-concerns rule; docs/DEPLOYMENTS.md
  as the downstream operating manual; docs/QUICKSTART-VISION.md
  staking the future one-command-deploy capability.
- Surgical fix to SPEC.md opening so the spec no longer asserts
  OHM is the corpus the framework produces.
- SPEC.md §19.2 gains the 'Deployment-supplied subject framing'
  candidate that drives the 0.3.0 release.
This commit is contained in:
Ben Stull
2026-05-26 06:37:48 -07:00
parent 46049d296b
commit 29c96ea300
14 changed files with 1027 additions and 37 deletions
+93
View File
@@ -0,0 +1,93 @@
# 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.2.0 — 2026-05-26
**Breaking config change.** The frontend now requires
`VITE_APP_NAME` to be set at build time. `npm run build` fails with a
clear message if it is missing. There is no default; every deployment
names itself.
### Upgrade steps (from 0.1.0)
1. The deployment **MUST** create a `frontend/.env` file before
building. See `frontend/.env.example` for the contract.
2. The deployment **MUST** set `VITE_APP_NAME` in `frontend/.env` to
the user-visible name it wants to ship — the string used as the
browser tab title, the header brand, and the landing H1. The
build will fail loudly if this is unset or blank.
3. The deployment **MUST** rebuild the frontend (`npm install && npm
run build`) and redeploy the resulting bundle. The previous
bundle does not read `VITE_APP_NAME`.
4. The deployment **SHOULD** verify the name appears correctly in
the browser tab and the header after redeploy before bumping its
`.rfc-app-version` pin to `0.2.0`.
5. The deployment **MAY**, in the same upgrade, take the opportunity
to retire any local overrides it was using to brand the
pre-0.2.0 hardcoded strings — those overrides are now obsolete.
6. The deployment **MUST NOT** continue to expect graduation step 4
to fail on Gitea's "Please try again later" race; that path is
now handled by `wait_for_mergeable` + bounded retry. Any local
workaround retrying graduation at the deployment level is now
redundant and **SHOULD** be removed.
### Fixed
- **Graduation merge race.** §13.3 step 4 (`merge_pr`) used to call
Gitea's merge endpoint the instant step 3 returned, which races
Gitea's background mergeability computation and produces the
`405 "Please try again later"` response. Step 4 now waits for the
`mergeable` field to become non-null (bounded by a 30s timeout) and
retries the merge call up to three times on the transient response.
See `backend/app/gitea.py#wait_for_mergeable` and
`backend/app/bot.py#_merge_with_retry`.
### Changed
- `frontend/src/App.jsx` header brand reads `VITE_APP_NAME` instead of
a hardcoded string.
- `frontend/src/components/Landing.jsx` H1 reads `VITE_APP_NAME`
instead of a hardcoded string. The pitch / deck / attribution copy
in this file is still deployment-specific and tracked as a follow-up
for extraction into deployment config.
- `frontend/index.html` `<title>` is rewritten at build time via Vite's
HTML transform.
### Added
- `frontend/.env.example` documenting the new required variable.
- Top-level `VERSION` file and this `CHANGELOG.md` as the canonical
release log.
- `SPEC.md` §20 (versioning and downstream deployments) as the
binding policy for the framework/deployment relationship.
- `docs/DEPLOYMENTS.md` as the practical guide for building on
rfc-app and upgrading existing deployments to new versions.
- `CLAUDE.md` at the repo root capturing the separation-of-concerns
rule for working sessions.
## 0.1.0 — v1 build
Initial release. See `docs/DEV.md` for the slicing plan and build
history.
+80
View File
@@ -0,0 +1,80 @@
# Working in rfc-app
This is the framework — the software that hosts an RFC standardization
process. It is intended to power **multiple deployments**, each of which
brands and configures itself independently. The framework is the
substrate; the deployment is the instance.
## The separation-of-concerns rule
Code in this repo must never bake in values that belong to a specific
deployment. That includes:
- The deployment's display name, tagline, or pitch copy.
- The corpus or domain the deployment is about.
- The deployment's URL, organization name, branding assets.
- Any reference to the people or community running a specific instance.
When a value of that kind is needed in source, it goes behind
configuration — typically a build-time `VITE_*` env var the frontend
reads, or a backend env var the API surfaces. The framework supplies a
loud failure path (e.g. build fails, server refuses to start) when a
required value is missing, so a misconfigured deployment can't ship the
wrong content silently.
The framework name itself (`Wiggleverse RFC`, per the founding
documents) does appear in source — in the chat system prompt, in
`PHILOSOPHY.md`, in `SPEC.md`. That is correct: those references are
about *the framework*, not about any deployment. Deployment names and
deployment content stay out.
If you find a hardcoded deployment-specific string in source, treat it
as a bug to fix in the framework: make it configurable, document the
new env var in `frontend/.env.example` or `backend/.env.example`, and
bump the version per the rules below.
## Versioning
The binding policy is in [`SPEC.md` §20](./SPEC.md). Summary so
working sessions don't have to reload the full section:
- The `VERSION` file at the repo root is the canonical version;
`frontend/package.json#version` mirrors it. A divergence between
the two is a spec-level bug.
- SemVer: patch = no operator action required; minor = new
functionality plus, while pre-1.0, breaking changes spelled out
in the changelog; major = reserved for the first stable cut and
afterwards for breaking changes.
- Every release adds an entry to `CHANGELOG.md`. Breaking entries
carry an "upgrade steps" block; a downstream deployment must be
able to upgrade from any prior version by walking those blocks in
order.
The practical recipe for downstream projects — how to pin, how to
upgrade, how the two-repo loop runs — is in
[`docs/DEPLOYMENTS.md`](./docs/DEPLOYMENTS.md). Don't restate that
recipe here; if it's wrong, fix it there.
## How changes typically flow in
The framework receives change requests from deployments. The intake
loop:
1. A deployment operator notices something they want different
(different name, missing feature, wrong behavior).
2. Triage: is this a framework concern (bug, missing affordance, hard-
coded value that should be configurable) or a deployment concern
(just a config value to change)?
3. If framework: implement here, bump the version, update the changelog
with upgrade steps.
4. The deployment then pins to the new version and applies any required
config changes.
The framework does not name specific deployments, but it does aim to
make each deployment's upgrade path obvious and mechanical.
## Code layout
See `docs/DEV.md` for the slicing plan and build history. See
`SPEC.md` for the binding contract — section numbers (`§n.n`) in the
codebase refer to that file.
+228 -8
View File
@@ -1,14 +1,21 @@
# RFC App — Specification
This is the agreed-upon model for the Wiggleverse RFC Contributor app —
the host for the **Open Human Model (OHM)**, the corpus of RFCs the
framework produces. Each RFC defines one word; the first defines
*human*. OHM is English-first by design: the markdown bodies are
canonical, and the OpenXML APIs and UX surfaces a downstream system
needs to actually let humans and machines interact are derived from
that English source, not authored alongside it. The framework's *why*
lives in [`PHILOSOPHY.md`](./PHILOSOPHY.md); this document is the
binding *what* for the app that hosts OHM.
a substrate for collaborative, RFC-style standardization of whatever
subject a given deployment chooses to standardize. A *deployment*
brings a corpus, a domain framing, and a brand; the framework provides
the process, the storage, the conversation, and the lifecycle. Each
RFC defines one entry in the corpus — a word, a vaccine, a literary
work, a protocol, a concept; the framework does not constrain the
subject. The first deployment of this framework, the Open Human Model
(OHM), standardizes natural-language vocabulary and is the corpus
referenced as an example throughout these pages; the framework itself
is independent of any one corpus. Markdown bodies are the canonical
text per RFC; any downstream APIs or UX surfaces a deployment exposes
are derived from those bodies, not authored alongside them. The
framework's *why* lives in [`PHILOSOPHY.md`](./PHILOSOPHY.md); this
document is the binding *what* for the app. The relationship between
the framework and its downstream deployments is set out in §20.
It captures the structural decisions made before any UX work on the main document
pane, per-RFC conversations, revisions, and PRs. Those areas are deliberately
@@ -3201,6 +3208,47 @@ binding.
read-only relationship counts as active). A future session may
settle a tighter definition (e.g., has any `actions` row on the
RFC) if the generous proxy refuses too many legitimate mutes.
- **Deployment-supplied subject framing.** The framework was built
with one deployment in mind (OHM, standardizing natural-language
vocabulary), but the substrate generalizes to any domain that
benefits from collaborative, transcript-evidenced standardization:
medicine (a public, model-assisted process for revising the DSM's
diagnostic criteria), law (model statutes, regulatory definitions,
or clause libraries), bioethics, biosecurity (vaccine protocols),
literature (canonical-edition standardization), software (library
API contracts written in prose), policy (municipal ordinance
templates), education (curriculum standards). Several surfaces in
the framework still bake OHM's subject in:
`backend/app/chat.py` SYSTEM_PROMPT names the framework "a
standardization process for natural-language vocabulary" and tells
the model the transcript is "the durable evidence the definition
was earned"; `frontend/src/components/Landing.jsx` pitches the
framework as "an open dictionary of the words humans and machines
need to agree on" with deck items asserting "One word per RFC";
`frontend/src/components/ProposeModal.jsx` field-help says "The
word or topic this RFC will define"; the default
`PHILOSOPHY.md` argues for the framework entirely in
natural-language-vocabulary terms. A different deployment — one
whose RFCs define vaccines, literary works, protocols, or
anything else — would have the AI mis-modeling the domain on
every turn and the user-facing copy claiming the wrong thing.
The settling session lands the framework primitive that's
missing: a deployment-supplied "subject" (probably
`VITE_RFC_SUBJECT_SINGULAR` and `VITE_RFC_SUBJECT_PLURAL` for
user-facing strings, plus a backend `RFC_DOMAIN_FRAMING` string
injected into the SYSTEM_PROMPT in place of the hardcoded
vocabulary clause) and lifts the OHM-specific Landing copy /
PHILOSOPHY content out of the framework into deployment config
(probably a `landing.json` the deployment provides and a
`PHILOSOPHY_PATH` override the deployment supplies). Touches the
opening of this spec (already noted: lines around §3 and §16
still reference OHM directly), §1 (which currently names "Open
Human Model" indirectly via `PHILOSOPHY.md`), §14.1 (the landing
page), and §18 (the chat surface). Goes out as the 0.3.0 release
with explicit MUST steps for OHM to set the new env vars and
supply its own Landing / PHILOSOPHY content. Worth its own
session; the analysis is in this candidate so the session can
open with design rather than discovery.
Topic 13 (notifications) is settled and folded into §5 (the
notifications, watches, branch_chat_seen, notification_user_mutes,
@@ -3233,3 +3281,175 @@ session and any sessions after it run on a modified shape:
on the original queue agreement (drive to decision, fold,
update §19.2), and need not be sequential — multiple §19.2
topics can be settled in any order the user prefers.
## 20. Versioning and downstream deployments
The framework is intended to host multiple deployments — separate
instances run by separate operators, each with its own brand, content,
and configuration. The Open Human Model corpus referenced in the
opening of this document is one such deployment; nothing in the
framework code, schema, or surfaces should depend on a particular
deployment existing. Code that bakes in a deployment-specific value
is a bug to fix in the framework: make the value configurable, ship a
new version, and let each deployment supply its own.
This section sets the contract by which downstream deployments depend
on the framework: how the framework is versioned, what each kind of
version change promises and breaks, and how a deployment upgrades.
The practical recipe — files to create, scripts to run, where the pin
lives — is in [`docs/DEPLOYMENTS.md`](./docs/DEPLOYMENTS.md), which
deployments treat as the operating manual; this section is the
binding policy the manual implements.
### 20.1 Canonical version
The framework's current version is the contents of the `VERSION` file
at the repo root, written as a SemVer string with no leading `v`. The
`VERSION` file is the single source of truth; everything else mirrors
it. In particular, `frontend/package.json#version` is kept in sync
with `VERSION` on every release. A release where the two diverge is a
spec-level bug.
### 20.2 SemVer rules
Versions follow [Semantic Versioning](https://semver.org/) as
`MAJOR.MINOR.PATCH`. The framework is pre-1.0 until the first stable
cut; while pre-1.0, the rules below apply with one concession:
breaking changes are allowed at minor-version increments, and the
changelog (§20.4) carries the upgrade burden instead of the version
number.
A **patch** bump (`0.x.0 → 0.x.1`) is reserved for bug fixes that
require no action from any downstream deployment beyond rebuilding
and redeploying. No new env vars; no schema changes; no API surface
changes; no behavior changes that a deployment would notice in a
meaningful way. If an operator can apply the new version without
reading the changelog and have everything keep working, it is a
patch.
A **minor** bump (`0.x.y → 0.(x+1).0`) carries either backward-
compatible additions (new endpoints, new optional config, new
features that default to off or to the prior behavior) or, while
pre-1.0, breaking changes that the changelog documents explicit
upgrade steps for. Once the framework reaches 1.0, breaking changes
move to the major lane and minor bumps regain their strict additive
meaning.
A **major** bump (`0.x.y → 1.0.0`, then `1.0.0 → 2.0.0`) marks the
first stable cut and, afterwards, releases that break a deployed
instance unless its operator takes action. Major bumps always pair
with an upgrade-steps block in the changelog and may pair with
migration scripts checked into `scripts/`.
### 20.3 What is in the framework's surface
The version line covers any aspect of the framework that a downstream
deployment can observe or depend on. Concretely: the schema in `§5`,
the HTTP surface in `§17`, the backend env contract (the variables
documented in `backend/.env.example` and their semantics), the
frontend build contract (the variables documented in
`frontend/.env.example` and how they reach the bundle), the §6 / §9 /
§10 / §13 lifecycle behaviors a Gitea-watching operator would notice,
and the on-disk shapes the framework writes into a deployment's Git
repositories (RFC.md, README.md, `.rfc/metadata.yaml`, the entry
frontmatter per §2).
Things outside the version line: documentation phrasing, internal
refactors that do not change observable behavior, test
reorganization, comments, and dev-only tooling. Changes confined to
those areas may ship without a version bump and without a changelog
entry.
### 20.4 Changelog discipline
Every release adds an entry to `CHANGELOG.md`, in the format the
existing entries demonstrate: a header line with version and date, a
narrative summary of what changed, and — for any release that
requires deployment action — an explicit "upgrade steps" block
listing the actions in order.
Upgrade-steps blocks **shall** be written using the RFC 2119 / RFC
8174 normative-language convention, with the keywords interpreted as
defined in those RFCs. In the context of an upgrade from version `N`
to version `N+1`:
- A step worded with **MUST**, **SHALL**, or **REQUIRED** is an
action without which the deployment will not function correctly on
the new version. Skipping such a step is a regression the framework
does not commit to handling. Building or starting the deployment
after skipping a MUST step may fail loudly (which is the framework
obligation in §20.6); it may also fail subtly later. Either way,
the deployment is out of contract until the step is performed.
- A step worded with **MUST NOT** or **SHALL NOT** is an action that
was previously valid but is no longer supported. Continuing to do
it after the upgrade is a regression the framework does not commit
to handling.
- A step worded with **SHOULD** or **RECOMMENDED** is the framework's
recommended action, taken in testing and assumed in documentation.
A deployment may deviate when it has a reason, but accepts that
framework behavior is defined against the recommended path; non-
recommended choices may interact poorly with later releases.
- A step worded with **SHOULD NOT** or **NOT RECOMMENDED** is the
inverse: actions the framework discourages without forbidding.
- A step worded with **MAY** or **OPTIONAL** is an affordance the
release exposes. A deployment can take it or skip it; both are
equally supported, and the framework's behavior does not depend on
the choice.
This convention is binding because it lets each adjacent version-step
be unambiguous in isolation. Cross-version upgrades — moving a
deployment from version `A` to version `B` where `B > A + 1` — **are
not pre-computed**; instead, the operator (or future tooling)
composes the upgrade by concatenating the MUST / SHOULD / MAY steps
from each adjacent release in the range `(A, B]` in order, applying
them as a single plan. Because each adjacent step is locally
unambiguous, the composition is mechanical; the framework's
obligation is to keep each adjacent step's language precise, not to
maintain a table of every A-to-B path.
If an upgrade step touches the deployment's environment (new env var,
changed default, removed var), the corresponding `*.env.example` file
**must** be updated in the same release so the contract and the
documentation land together.
### 20.5 The deployment pin
Each downstream deployment pins to a specific framework version. The
pin lives in the deployment's own repo as a single-line file named
`.rfc-app-version`, containing the SemVer string the deployment is
currently built against. The pin is the deployment's promise to its
own users about what behavior is in flight; bumping it is a
deliberate operator action that pairs with applying any upgrade
steps the new version requires.
When an operator decides to upgrade, the sequence is: read
`CHANGELOG.md` for every version between the current pin and the
target; apply each release's upgrade steps in order against the
deployment's environment; build the framework at the target version;
deploy; bump `.rfc-app-version` in the deployment repo to the target.
### 20.6 The framework's obligations to deployments
The framework, in exchange for being the substrate, commits to:
- not bake deployment-specific values into source — every per-
deployment value goes behind config the deployment supplies;
- ship a loud failure path (build fails, server refuses to start,
visibly missing UI) when a deployment forgets to supply a required
value, rather than a silent default that ships the wrong thing;
- keep the changelog honest about what each release breaks and how to
fix it;
- not regress a deployment that follows the upgrade-steps faithfully.
### 20.7 The deployment's obligations to the framework
Downstream deployments, in exchange for the contract above, commit to:
- pin to a specific framework version (the `.rfc-app-version` file);
- not fork the framework to embed deployment-specific behavior — if a
deployment needs the framework to change, the change goes upstream,
the framework ships a new version, and the deployment upgrades;
- read the changelog before bumping, and apply upgrade steps in
order;
- supply every required env var the framework documents at the
version they are running.
+1
View File
@@ -0,0 +1 @@
0.2.0
+78 -7
View File
@@ -22,11 +22,67 @@ anywhere.
"""
from __future__ import annotations
import asyncio
import json
import logging
from dataclasses import dataclass
from . import db, notify
from .gitea import Gitea
from .gitea import Gitea, GiteaError
log = logging.getLogger(__name__)
_MERGE_TRANSIENT_HINTS = (
"please try again later",
"is not ready",
"not ready to be merged",
)
async def _merge_with_retry(
gitea: Gitea,
org: str,
meta_repo: str,
pr_number: int,
*,
merge_message_title: str,
merge_message_body: str,
attempts: int = 3,
delay_seconds: float = 1.0,
) -> None:
"""Call gitea.merge_pull and retry on Gitea's transient
"Please try again later" / "not ready" responses. After
`wait_for_mergeable` returns this is rare, but Gitea has been
observed to flip back to the unready state momentarily on a
write-amplified instance; a small bounded retry closes the gap.
Any other GiteaError propagates immediately so the §13.3
orchestrator can fail the step and run rollback.
"""
last_error: GiteaError | None = None
for attempt in range(1, attempts + 1):
try:
await gitea.merge_pull(
org, meta_repo, pr_number,
merge_message_title=merge_message_title,
merge_message_body=merge_message_body,
style="merge",
)
return
except GiteaError as e:
detail = (e.detail or "").lower()
if not any(hint in detail for hint in _MERGE_TRANSIENT_HINTS):
raise
last_error = e
log.warning(
"merge_pull transient (attempt %d/%d) for %s/%s#%d: %s",
attempt, attempts, org, meta_repo, pr_number, e.detail,
)
if attempt < attempts:
await asyncio.sleep(delay_seconds)
assert last_error is not None
raise last_error
@dataclass(frozen=True)
@@ -823,14 +879,29 @@ class Bot:
) -> None:
"""§13.3 step 4: auto-merge the graduation PR with the admin as
merge actor. Distinct action_kind so the audit log carries the
graduation as a linkable sequence per §13.3's transactional shape."""
graduation as a linkable sequence per §13.3's transactional shape.
Gitea computes a PR's mergeability asynchronously in a background
job. Step 3 (`open_graduation_pr`) returns the moment the PR is
created, which is typically before that job has finished — calling
`merge_pull` immediately produces a `405 "Please try again later"`
and the §13.3 orchestrator interprets it as a terminal failure.
We wait for the mergeability computation to settle before the
merge call, then retry the merge a couple of times if Gitea still
reports the transient state (belt-and-suspenders against the same
race appearing in a different shape).
"""
subject = f"Graduate {slug}{rfc_id}"
body = _trailer(actor)
await self._gitea.merge_pull(
org, meta_repo, pr_number,
merge_message_title=subject,
merge_message_body=body,
style="merge",
try:
await self._gitea.wait_for_mergeable(org, meta_repo, pr_number)
except TimeoutError as e:
raise GiteaError(409, str(e)) from e
await _merge_with_retry(
self._gitea, org, meta_repo, pr_number,
merge_message_title=subject, merge_message_body=body,
)
_log(
actor,
+41
View File
@@ -12,13 +12,17 @@ and never reaches around it.
"""
from __future__ import annotations
import asyncio
import base64
import logging
from typing import Any
import httpx
from .config import Config
log = logging.getLogger(__name__)
class GiteaError(Exception):
def __init__(self, status: int, detail: str):
@@ -247,6 +251,43 @@ class Gitea:
},
)
async def wait_for_mergeable(
self,
owner: str,
repo: str,
number: int,
*,
timeout_seconds: float = 30.0,
poll_interval_seconds: float = 0.5,
) -> bool:
"""Block until Gitea finishes computing the PR's mergeability,
or the timeout expires.
Gitea computes the `mergeable` field in a background job after a
PR is opened or its head commit changes. Until that job finishes,
`mergeable` is `null` and `POST /pulls/{n}/merge` rejects with
`405 "Please try again later"`. Callers about to merge should
wait here first so the merge call doesn't race the computation.
Returns the final `mergeable` value (True / False). Raises
TimeoutError if the field never becomes non-null within the
budget — the caller can then surface a retryable error to the
operator instead of silently failing the merge step.
"""
deadline = asyncio.get_event_loop().time() + timeout_seconds
while True:
pr = await self.get_pull(owner, repo, number)
if pr is not None:
mergeable = pr.get("mergeable")
if mergeable is not None:
return bool(mergeable)
if asyncio.get_event_loop().time() >= deadline:
raise TimeoutError(
f"Gitea did not compute mergeability for "
f"{owner}/{repo}#{number} within {timeout_seconds:.1f}s"
)
await asyncio.sleep(poll_interval_seconds)
async def close_pull(self, owner: str, repo: str, number: int) -> None:
await self._request(
"PATCH",
+253
View File
@@ -0,0 +1,253 @@
# Deployments — building on rfc-app and keeping in sync
This document is the operating manual for downstream projects that
host an instance of the RFC framework. It is the practical
counterpart to [`SPEC.md` §20](../SPEC.md), which carries the
binding policy. If anything here disagrees with §20, §20 wins and
the discrepancy is a bug in this doc.
The audience is the operator of a downstream deployment — someone
who wants to run their own corpus on top of rfc-app and keep that
deployment current as the framework releases new versions.
## What is a "deployment"?
A deployment is a running instance of the framework, hosting a
specific corpus, branded and configured for a specific community.
Each deployment has:
- **A content repo** (separate from this one). Typically holds the
initial RFC markdown files, a `README.md` describing what the
deployment is, a `CONTRIBUTING.md`, and a `.rfc-app-version`
pin. The Open Human Model (`ohm-rfc`) is one such repo.
- **A Gitea instance** the framework talks to. Hosts the meta repo
and the per-RFC repositories the graduation flow creates.
- **A server** running the rfc-app backend + frontend bundle,
pointed at the Gitea instance, with `frontend/.env` and
`backend/.env` configured for this deployment.
- **A pin** — the `.rfc-app-version` file in the deployment's
content repo, declaring which framework version is currently in
flight.
The framework repo (this one) is generic across deployments. The
deployment supplies the values, the content, and the operational
choices. The framework supplies the substrate.
## Starting a new deployment
The shortest path from scratch:
1. **Create the deployment's content repo.** Anywhere git lives —
GitHub, Gitea, GitLab. It will hold the initial RFC markdown
files and the deployment's public-facing README. Add a
`.rfc-app-version` file with the framework version you intend to
pin to (the current `VERSION` from this repo, or any prior
version if you have a reason to pin behind).
2. **Stand up Gitea.** Anything that exposes the Gitea REST API
works; `deploy/RUNBOOK.md` in this repo describes the bring-up.
Create the bot service account and the OAuth2 application.
3. **Check out rfc-app at the version your deployment pins to.**
`git checkout` the tag (or commit) matching the
`.rfc-app-version` you chose. The pin is a contract — your
deployment's behavior is defined by *that* version's code, not
`main`.
4. **Configure the backend.** Copy `backend/.env.example` to
`backend/.env` and fill in the Gitea URL, bot token, OAuth
client credentials, `GITEA_ORG`, `META_REPO`, OWNER login, and
any optional providers (SMTP, LLM keys). The `.env.example` is
the contract for what the framework expects at this version;
read every entry.
5. **Configure the frontend.** Copy `frontend/.env.example` to
`frontend/.env` and fill it in. At minimum, every release
requires `VITE_APP_NAME` (the user-visible name of your
deployment — what it gets called in the browser tab, the header
brand, and the landing H1). The framework ships no defaults for
any deployment-identifying value; if a required variable is
missing, the build fails loudly.
6. **Build and run.** `cd frontend && npm install && npm run
build`, then start the backend per `deploy/RUNBOOK.md`. The
first sign-in is the OWNER login from `backend/.env`.
7. **Seed the meta repo.** `python scripts/seed_meta_repo.py`
creates the meta repo on Gitea with the initial README,
CONTRIBUTING, and `.gitignore` per §1–§2. This step is one-time
per deployment.
8. **Bring in your content.** Open proposal PRs against the meta
repo to add your initial RFCs, or — if you're migrating an
existing corpus — write directly into the meta repo's `rfcs/`
directory at the version of `.rfc/metadata.yaml` your pinned
framework version expects.
At this point the deployment is running. The content repo holds
the canonical text and the `.rfc-app-version` pin; the server runs
the framework at that pin against your Gitea; the framework knows
nothing about your specific deployment beyond what your `.env`
files told it.
## The two-repo working pattern
Once a deployment is live, day-to-day changes split across the
framework repo and the deployment repo. The triage that decides
which side a given change lands on:
A change is **deployment-side** when it is about a value, a piece
of content, or an operational choice specific to this instance.
Examples: changing the display name; updating an RFC's body;
adding a new RFC; rotating Gitea credentials; tightening SMTP
settings; changing who has the OWNER role.
A change is **framework-side** when it is about behavior, UI
logic, schema, or any piece of code that would matter the same way
to a different deployment. Examples: fixing a bug; adding a new
feature; making a previously hardcoded value configurable; tuning
a retry; correcting a race condition; updating a dependency that
affects observable behavior.
The most common case is **mixed**: the framework needs to make
something configurable, and the deployment needs to supply the
value. The change pattern is then: framework adds the env var (or
config knob) and the loud-failure path, ships a new minor or
patch; the deployment upgrades to the new version and supplies
the value in its `.env`.
When picking up a change, name it in deployment terms first ("in
*my deployment* I want X"), then triage. The phrasing keeps the
work honest — every framework change is in service of something
someone wants from their deployment.
## Upgrading to a new framework version
The upgrade contract is in [`SPEC.md` §20.5](../SPEC.md), and the
normative language used in upgrade-steps blocks is fixed in §20.4.
The mechanics in practice:
1. **Decide the target version.** Read `CHANGELOG.md` from the
current pin up through `main`. Pick the target — usually the
latest, but pinning behind is legitimate if a release introduces
something the deployment isn't ready for.
2. **Read every intervening release's upgrade steps.** Each release
header in `CHANGELOG.md` includes an "upgrade steps" block when
there is operator action required. Read them in order; the
composition is your upgrade plan. **Cross-version upgrades are
not pre-computed** (see §20.4) — going from 0.1.0 to 0.5.0 means
walking the upgrade-steps for 0.2.0, then 0.3.0, then 0.4.0,
then 0.5.0 in that order, applying them as a single plan.
3. **Triage the plan by normative language.** Each step is worded
per RFC 2119 / 8174:
- **MUST / SHALL / REQUIRED** steps are non-negotiable. Skip one
and the deployment is out of contract on the new version.
Either it will fail to build or start (the framework's
§20.6 commitment to loud failures), or it will misbehave
subtly later. Treat these as the binding part of your plan.
- **SHOULD / RECOMMENDED** steps are the framework's tested
default. You can deviate when you have a reason, but
subsequent releases assume you took the recommended path; a
deviation now may complicate a future upgrade.
- **MAY / OPTIONAL** steps are affordances. Take them or skip
them based on your needs; the framework's behavior doesn't
depend on the choice.
- **MUST NOT / SHOULD NOT** steps remove or discourage prior
behavior. Check whether your deployment was relying on the
called-out behavior; if so, that's the thing to change.
4. **Apply the upgrade steps in order against the deployment's
environment.** New env vars get added to `frontend/.env` or
`backend/.env`. Renamed vars get renamed. Removed vars get
removed. Migration scripts (if any are referenced) get run.
5. **Check out the framework at the target version.** `git
fetch && git checkout <tag>`.
6. **Rebuild.** `npm install && npm run build` for the frontend;
restart the backend.
7. **Smoke-test.** Sign in, check the brand reflects your
`VITE_APP_NAME`, exercise whatever surface the release touched.
Walk through the SHOULD steps that involve verification.
8. **Update the pin.** Change `.rfc-app-version` in the deployment
repo to the target version. Commit. This is the deployment
promising its users that *this* is the framework version
currently in flight.
If a step fails, stop. Do not bump the pin. The framework's
obligation per §20.6 is that a faithful upgrade does not regress
the deployment — failure is a bug to report against the framework
release, not something to paper over.
### Composing across multiple versions
Worked example: a deployment pinned to `0.1.0` wants to move to
some future `0.4.0`. The plan is the concatenation of three
adjacent upgrade-steps blocks from `CHANGELOG.md`:
```
0.1.0 → 0.2.0: MUST set VITE_APP_NAME, MUST rebuild, SHOULD verify
0.2.0 → 0.3.0: (whatever the 0.3.0 entry's steps are)
0.3.0 → 0.4.0: (whatever the 0.4.0 entry's steps are)
```
The operator concatenates these into one plan, applies them in
order, rebuilds once at the end against `0.4.0`'s checkout, and
bumps the pin to `0.4.0`. No path-specific instructions are
needed because each adjacent step is unambiguous in isolation.
This is why §20.4 binds the normative-language convention: the
composition only works mechanically if each adjacent step's
strength is clear from its wording.
## Versioning, briefly
[`SPEC.md` §20.2](../SPEC.md) is the binding rule. Summary:
- **Patch** (`0.x.0 → 0.x.1`): redeploy and you're done. No env
changes, no behavior surprises.
- **Minor** (`0.x.0 → 0.(x+1).0`): read the changelog. New optional
config, new features, and — while pre-1.0 — breaking changes
that the changelog spells out.
- **Major** (`0.x.0 → 1.0.0`, then `1.0.0 → 2.0.0`): always read
the changelog. Breaking by definition; upgrade steps required.
The framework is pre-1.0 today. Expect minor bumps to occasionally
require changes to your `.env` files until the first stable cut.
## The deployment's repo, at a glance
A deployment's content repo typically holds:
- The RFC markdown files (one per concept the deployment defines).
- A `README.md` describing what the deployment is, public-facing.
- A `CONTRIBUTING.md` describing how to participate.
- `.rfc-app-version` — the framework pin (single line, SemVer).
- A `CLAUDE.md` describing how to work on the deployment, the
two-repo iteration pattern, and the deployment-specific values
(typically without secrets — those live on the server only).
The deployment repo does **not** hold:
- Any of the framework's code. The framework lives in its own repo;
you build against it at the pinned version.
- Secrets. `.env` files belong on the server, never in git.
- Forked copies of framework files. If you find yourself wanting to
edit a framework file, the right move is to file a change against
the framework, get a release, and pin to it.
## When something goes wrong
If a framework behavior is wrong for your deployment, file it as a
framework change request: a clear statement of what you wanted, what
happened instead, and (if you can) what the right separation of
concerns is between framework and deployment.
If a framework upgrade breaks your deployment after you followed the
changelog faithfully, that's a §20.6 violation — file it against the
framework release. The pin lets you roll back to the prior version
while the framework fixes the regression.
+188
View File
@@ -0,0 +1,188 @@
# Vision: a one-command path to a new rfc-app deployment
This document is **not** an implementation plan. It is the vision that
the current build does not yet deliver — the experience we want for
anyone who reads about the RFC framework and decides they'd like to
run their own. Today, standing up a deployment is the multi-step
journey in [`DEPLOYMENTS.md`](./DEPLOYMENTS.md). The aspiration in
this doc is to compress that journey to a single command (or a small
handful), so the friction between *I want to run an instance of this*
and *I have a working instance of this* is as close to zero as we can
get it.
We are writing this down now so the principles are settled before any
code lands.
## Who this is for
The framework is the substrate for collaborative, RFC-style
standardization of any subject where a community needs to agree on
the meaning of its terms and where the argument that produced the
agreement is itself worth preserving. The deployments we imagine the
quickstart serving include — but are not limited to:
- **Medicine.** A public, model-assisted revision of the DSM's
diagnostic criteria. Each diagnosis becomes an RFC; the criteria
and their boundaries are argued in transcript; graduation gives a
diagnosis a stable identifier and a reviewable history of how it
came to be defined the way it is. The same shape applies to
clinical protocols, drug-interaction taxonomies, and rare-disease
registries.
- **Law.** Model statutes, regulatory definitions, or clause
libraries. An RFC per clause, with the drafting argument
preserved alongside the final text. The framework's audit log
doubles as legislative history.
- **Bioethics and biosecurity.** Vaccine protocols, gene-editing
guidelines, dual-use research review criteria. Domains where
consensus saves lives and the reasoning behind the consensus
matters as much as the consensus itself.
- **Literature.** Canonical-edition standardization for works with
contested manuscripts; lexicons for technical or constructed
languages; controlled vocabularies for archives.
- **Software.** Library API contracts written as prose, before the
code. RFC-style definitions of cross-language behaviors that
multiple implementations must respect.
- **Policy and governance.** Municipal ordinance templates, NGO
governance rubrics, standards-body procedural definitions.
- **Education.** Curriculum standards, assessment rubrics, the
meaning of competencies and credentials.
- **Natural language itself.** The original case — the Open Human
Model deployment — standardizing the vocabulary humans and
machines need to share.
The quickstart's goal is to make the path from *"I run one of these
communities and we need this"* to *"my community has a working
deployment to start arguing on"* short enough that the choice is
about the work the community wants to do, not the operational lift
of getting started.
## The intended experience
A reader who has just finished `PHILOSOPHY.md` and decides they want
their own deployment should be able to:
1. Pick a name for their deployment (one user-visible string).
2. Run a single command from a clean machine — `npx
create-rfc-deployment <name>`, or `docker compose up` from a tiny
template, or `curl ... | bash`, or whatever shape the design
settles on.
3. Answer a small number of questions the command can't infer:
their Gitea endpoint (or an offer to spin one up locally), an
admin email, the OAuth callback URL.
4. End up with: a running rfc-app instance reachable in a browser, a
meta repository seeded with the right structure, a first sign-in
that hands the OWNER role to the operator, and a deployment repo
on disk holding their `.rfc-app-version` pin, their `.env`
defaults (sans secrets), and a `CLAUDE.md` describing the
two-repo loop.
From the operator's point of view, the framework should feel like
infrastructure they instantiated, not a codebase they cloned.
## Why this isn't there yet
The current bring-up requires the operator to do work the framework
could do for them. Specifically:
- **Gitea provisioning.** The operator stands up Gitea by hand,
creates the bot account, generates the bot token, registers the
OAuth2 application, copies credentials into `backend/.env`. A
quickstart could spin up Gitea in a sibling container, create the
bot via Gitea's first-run admin API, and emit the credentials
straight into the deployment's environment.
- **Meta-repo seeding.** Today `scripts/seed_meta_repo.py` is a
manual step. A quickstart could run it as part of the first boot,
detect an already-seeded repo (idempotent), and report what it did.
- **Frontend `.env`.** The operator copies `.env.example` and hand-
fills `VITE_APP_NAME`. A quickstart could ask once and write the
file.
- **Deployment repo bootstrap.** Today the deployment repo is a
thing the operator creates manually. A quickstart could generate
it: a fresh git repo containing `.rfc-app-version`, a README
template, a `CONTRIBUTING.md` template, a `CLAUDE.md` template
cloned from this repo's pattern, and a placeholder
`rfcs/RFC-0001-...md` the operator fills in.
- **OAuth callback wiring.** The current bring-up requires the
operator to register an OAuth app in Gitea by hand. A quickstart
could do this through Gitea's API once the bot token exists.
- **Process supervision.** Today `deploy/RUNBOOK.md` walks an
operator through systemd / nginx by hand. A quickstart could ship
a Docker Compose file (or systemd unit templates) that bring up
backend + frontend + Gitea in one shot.
Each of these is a small lift on its own. Together they're a
quickstart product — and that's what this doc is staking out.
## Design principles for when the work gets picked up
The quickstart is downstream of every other contract in this repo.
The principles below keep it that way:
**The quickstart does not embed deployment values.** It collects
them from the operator and writes them where they belong (the new
deployment's repo, the server's `.env`, etc.). The framework remains
deployment-agnostic; the quickstart is a configurator, not a
template renderer that bakes in OHM-like choices.
**The quickstart respects `SPEC.md` §20.** It pins the new
deployment to the current framework version (the `VERSION` file).
Future upgrades go through the §20.5 sequence — the quickstart does
not invent a parallel upgrade path. If the quickstart needs to bump
the pin, it does so by editing `.rfc-app-version` in the deployment
repo, the same way an operator would by hand.
**The quickstart produces a deployment repo that an operator can
own.** The output is plain files in a git repo, not a runtime that
hides the configuration. An operator who never wants to think about
the quickstart again can read the generated `CLAUDE.md` and
`.rfc-app-version` and continue from there with `DEPLOYMENTS.md` as
their guide.
**Loud failures, not silent defaults.** If the operator declines to
supply a required value, the quickstart refuses to continue. We do
not ship a "demo" deployment that produces something running but
useless; the framework's separation-of-concerns rule is upstream of
the quickstart's UX.
**One thing at a time.** A quickstart that tries to handle every
deployment topology (single-machine, Kubernetes, managed Gitea,
self-hosted Gitea, Cloud Run, Fly, etc.) on day one is a project
that ships nothing. The first cut targets a single happy path —
likely a single-machine Docker Compose with a local Gitea — and the
others are §19.2-style follow-ups.
## Open questions to settle before building
When this work gets picked up, the design session will need to
settle:
- **Where does the quickstart live?** A separate repo? A `tools/`
directory in this repo? A published npm package? Each has trade-
offs (versioning, discoverability, upgrade story).
- **What's the shape of "one command"?** `npx create-rfc-deployment`,
`docker compose up`, `curl | bash`, a TUI installer, a web wizard
hosted by the framework's docs site? The shape determines the
audience.
- **Does the quickstart bring up Gitea or assume it exists?** Both
are valid; "bring up Gitea" is friendlier for new operators but
enlarges the quickstart's surface; "assume Gitea" is cleaner but
leaves a real bring-up step in the operator's lap.
- **How does the quickstart handle secrets?** Generate them and
write them to disk? Print them to stdout? Both? What's the
rotation story?
- **What does the upgrade story look like for an operator who used
the quickstart?** The pin lives in the deployment repo per §20.5;
the quickstart should never become a parallel upgrade path. But
it might offer an `rfc-app upgrade` command that just runs the
§20.5 sequence in one shot.
These are the conversations to have when the time comes. None of them
need answering today; this doc exists so that whoever picks this up
has the shape of the goal in front of them.
## Status
Not started. This doc is the seed. When work begins, it gets its own
§19.2 candidate in `SPEC.md` and the design session follows the
existing decision-and-fold pattern. Until then, [`DEPLOYMENTS.md`](./DEPLOYMENTS.md)
is the path an operator follows.
+15
View File
@@ -0,0 +1,15 @@
# Frontend build-time configuration. Copy to `frontend/.env` and fill in.
#
# Vite picks up VITE_* variables from these files at build (`npm run build`)
# and dev (`npm run dev`) time. Real `.env` files are gitignored; only this
# `.env.example` is committed.
# The user-visible name of this deployment. Used as the browser tab title,
# the header brand, and the landing page H1. Required — the framework
# ships no default on purpose so each deployment names itself. If unset,
# `npm run build` fails with a clear message.
#
# Examples:
# VITE_APP_NAME=Wiggleverse RFC
# VITE_APP_NAME=Wiggleverse Open Human Model
VITE_APP_NAME=
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Wiggleverse RFCs</title>
<title>%VITE_APP_NAME%</title>
</head>
<body>
<div id="root"></div>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "rfc-app-frontend",
"private": true,
"version": "0.1.0",
"version": "0.2.0",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -84,7 +84,7 @@ export default function App() {
<div className="app">
<header className="app-header">
<div className="app-brand">
<Link to="/">Open Human Model</Link>
<Link to="/">{import.meta.env.VITE_APP_NAME}</Link>
</div>
<div className="header-right">
{/* §14.3: the persistent About link. One word, no badge, no
+7 -10
View File
@@ -1,14 +1,11 @@
// Landing.jsx — §14.1's pre-login surface.
//
// This instance hosts the Open Human Model — the corpus the framework
// produces, per PHILOSOPHY.md. The page's three jobs per §14.1 remain:
// name what this thing is, pitch why someone would care, and offer
// the sign-in affordance. Title and subtitle frame OHM-the-corpus;
// the pitch (lifted from the top of PHILOSOPHY.md) does the argument;
// a small attribution line below the deck names Wiggleverse RFC as
// the underlying process so a reader can tell what's the corpus and
// what's the software hosting it. The secondary link to `/philosophy`
// is for the reader who is interested but needs more before signing in.
// The page's three jobs per §14.1: name what this thing is, pitch why
// someone would care, and offer the sign-in affordance. The deployment
// supplies its display name via VITE_APP_NAME (see frontend/.env.example);
// the subtitle and pitch below currently carry the corpus this instance
// hosts and should be lifted into deployment config in a follow-up so
// the framework source stays brand-neutral.
import { Link } from 'react-router-dom'
@@ -16,7 +13,7 @@ export default function Landing() {
return (
<div className="landing">
<div className="landing-inner">
<h1>Open Human Model</h1>
<h1>{import.meta.env.VITE_APP_NAME}</h1>
<p className="subtitle">
An open dictionary of the words humans and machines need to agree on.
</p>
+40 -9
View File
@@ -1,15 +1,46 @@
import { defineConfig } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
// In dev, the frontend runs on Vite's port and proxies the API
// (and /auth/*, /api/webhooks/*) to the FastAPI process.
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': 'http://localhost:8000',
'/auth': 'http://localhost:8000',
//
// VITE_APP_NAME is the user-visible name of this deployment — used in
// the browser tab title, the header brand, and the landing page H1.
// The framework intentionally ships no default: every deployment must
// supply its own name via `frontend/.env` so a fresh build never ships
// the wrong brand. See `frontend/.env.example`.
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const appName = env.VITE_APP_NAME
if (!appName || !appName.trim()) {
throw new Error(
'VITE_APP_NAME is required but not set. Copy frontend/.env.example to ' +
'frontend/.env and set VITE_APP_NAME=... before building. The framework ' +
'ships no default on purpose — each deployment names itself.'
)
}
return {
plugins: [
react(),
// Build-time HTML token substitution: `%VITE_APP_NAME%` inside
// `index.html` becomes the configured name. Vite already does this
// for env vars referenced as `%VITE_*%` in HTML, but we wire it
// explicitly so the failure mode (token left in title) is loud.
{
name: 'inject-app-name',
transformIndexHtml(html) {
return html.replace(/%VITE_APP_NAME%/g, appName)
},
},
],
server: {
port: 5173,
proxy: {
'/api': 'http://localhost:8000',
'/auth': 'http://localhost:8000',
},
},
},
}
})