diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8ec7302 --- /dev/null +++ b/CHANGELOG.md @@ -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` `` 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. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..21c6589 --- /dev/null +++ b/CLAUDE.md @@ -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. diff --git a/SPEC.md b/SPEC.md index 2cadf7e..30077ec 100644 --- a/SPEC.md +++ b/SPEC.md @@ -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. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..0ea3a94 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.2.0 diff --git a/backend/app/bot.py b/backend/app/bot.py index acad31b..4e116ab 100644 --- a/backend/app/bot.py +++ b/backend/app/bot.py @@ -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, diff --git a/backend/app/gitea.py b/backend/app/gitea.py index 598f6e4..4c626a5 100644 --- a/backend/app/gitea.py +++ b/backend/app/gitea.py @@ -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", diff --git a/docs/DEPLOYMENTS.md b/docs/DEPLOYMENTS.md new file mode 100644 index 0000000..98a5413 --- /dev/null +++ b/docs/DEPLOYMENTS.md @@ -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. diff --git a/docs/QUICKSTART-VISION.md b/docs/QUICKSTART-VISION.md new file mode 100644 index 0000000..752526f --- /dev/null +++ b/docs/QUICKSTART-VISION.md @@ -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. diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..92e36db --- /dev/null +++ b/frontend/.env.example @@ -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= diff --git a/frontend/index.html b/frontend/index.html index e57eaf0..46112f8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>Wiggleverse RFCs + %VITE_APP_NAME%
diff --git a/frontend/package.json b/frontend/package.json index a2e7235..565c486 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "rfc-app-frontend", "private": true, - "version": "0.1.0", + "version": "0.2.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a844182..2f6e3c2 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -84,7 +84,7 @@ export default function App() {
- Open Human Model + {import.meta.env.VITE_APP_NAME}
{/* §14.3: the persistent About link. One word, no badge, no diff --git a/frontend/src/components/Landing.jsx b/frontend/src/components/Landing.jsx index 605fdb1..8dc4987 100644 --- a/frontend/src/components/Landing.jsx +++ b/frontend/src/components/Landing.jsx @@ -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 (
-

Open Human Model

+

{import.meta.env.VITE_APP_NAME}

An open dictionary of the words humans and machines need to agree on.

diff --git a/frontend/vite.config.js b/frontend/vite.config.js index aaa2e40..76ca792 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -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', + }, }, - }, + } })