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:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
+78
-7
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"name": "rfc-app-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user